Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c0199c43cf | ||
|
|
7940b9a0a7 | ||
|
|
920914e3ee |
@@ -122,6 +122,7 @@ const en: Messages = {
|
||||
'tray.recentHosts': 'Recent Hosts',
|
||||
'tray.empty.title': 'Nothing here yet',
|
||||
'tray.empty.subtitle': 'Go connect to a server, they miss you 🚀',
|
||||
'tray.quit': 'Quit Netcatty',
|
||||
|
||||
// Vault Sidebar
|
||||
'vault.sidebar.collapse': 'Collapse sidebar',
|
||||
|
||||
@@ -107,6 +107,7 @@ const zhCN: Messages = {
|
||||
'tray.recentHosts': '最近连接的主机',
|
||||
'tray.empty.title': '一切都很安静',
|
||||
'tray.empty.subtitle': '去连接个服务器吧,它们想念你了 🚀',
|
||||
'tray.quit': '退出 Netcatty',
|
||||
|
||||
// Vault Sidebar
|
||||
'vault.sidebar.collapse': '收起侧边栏',
|
||||
|
||||
@@ -12,6 +12,11 @@ export const useTrayPanelBackend = () => {
|
||||
await bridge?.openMainWindow?.();
|
||||
}, []);
|
||||
|
||||
const quitApp = useCallback(async () => {
|
||||
const bridge = netcattyBridge.get();
|
||||
await bridge?.quitApp?.();
|
||||
}, []);
|
||||
|
||||
const jumpToSession = useCallback(async (sessionId: string) => {
|
||||
const bridge = netcattyBridge.get();
|
||||
await bridge?.jumpToSessionFromTrayPanel?.(sessionId);
|
||||
@@ -57,6 +62,7 @@ export const useTrayPanelBackend = () => {
|
||||
return {
|
||||
hideTrayPanel,
|
||||
openMainWindow,
|
||||
quitApp,
|
||||
jumpToSession,
|
||||
connectToHostFromTrayPanel,
|
||||
onTrayPanelCloseRequest,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { ChevronRight, FileSymlink, Folder, FolderOpen, Monitor, Server, Expand, Minimize2 } from 'lucide-react';
|
||||
import { CheckSquare, ChevronRight, FileSymlink, Folder, FolderOpen, Monitor, Server, Square, Expand, Minimize2 } from 'lucide-react';
|
||||
import React, { useMemo } from 'react';
|
||||
import { useI18n } from '../application/i18n/I18nProvider';
|
||||
import { useTreeExpandedState } from '../application/state/useTreeExpandedState';
|
||||
@@ -32,6 +32,9 @@ interface HostTreeViewProps {
|
||||
moveGroup: (sourcePath: string, targetPath: string) => void;
|
||||
managedGroupPaths?: Set<string>;
|
||||
onUnmanageGroup?: (groupPath: string) => void;
|
||||
isMultiSelectMode?: boolean;
|
||||
selectedHostIds?: Set<string>;
|
||||
toggleHostSelection?: (hostId: string) => void;
|
||||
}
|
||||
|
||||
interface TreeNodeProps {
|
||||
@@ -53,6 +56,9 @@ interface TreeNodeProps {
|
||||
moveGroup: (sourcePath: string, targetPath: string) => void;
|
||||
managedGroupPaths?: Set<string>;
|
||||
onUnmanageGroup?: (groupPath: string) => void;
|
||||
isMultiSelectMode?: boolean;
|
||||
selectedHostIds?: Set<string>;
|
||||
toggleHostSelection?: (hostId: string) => void;
|
||||
}
|
||||
|
||||
const TreeNode: React.FC<TreeNodeProps> = ({
|
||||
@@ -74,6 +80,9 @@ const TreeNode: React.FC<TreeNodeProps> = ({
|
||||
moveGroup,
|
||||
managedGroupPaths,
|
||||
onUnmanageGroup,
|
||||
isMultiSelectMode,
|
||||
selectedHostIds,
|
||||
toggleHostSelection,
|
||||
}) => {
|
||||
const { t } = useI18n();
|
||||
const isExpanded = expandedPaths.has(node.path);
|
||||
@@ -215,9 +224,12 @@ const TreeNode: React.FC<TreeNodeProps> = ({
|
||||
moveGroup={moveGroup}
|
||||
managedGroupPaths={managedGroupPaths}
|
||||
onUnmanageGroup={onUnmanageGroup}
|
||||
isMultiSelectMode={isMultiSelectMode}
|
||||
selectedHostIds={selectedHostIds}
|
||||
toggleHostSelection={toggleHostSelection}
|
||||
/>
|
||||
))}
|
||||
|
||||
|
||||
{/* Hosts in this group */}
|
||||
{sortedHosts.map((host) => (
|
||||
<HostTreeItem
|
||||
@@ -230,6 +242,9 @@ const TreeNode: React.FC<TreeNodeProps> = ({
|
||||
onDeleteHost={onDeleteHost}
|
||||
onCopyCredentials={onCopyCredentials}
|
||||
moveHostToGroup={moveHostToGroup}
|
||||
isMultiSelectMode={isMultiSelectMode}
|
||||
selectedHostIds={selectedHostIds}
|
||||
toggleHostSelection={toggleHostSelection}
|
||||
/>
|
||||
))}
|
||||
</CollapsibleContent>
|
||||
@@ -247,6 +262,9 @@ interface HostTreeItemProps {
|
||||
onDeleteHost: (host: Host) => void;
|
||||
onCopyCredentials: (host: Host) => void;
|
||||
moveHostToGroup: (hostId: string, groupPath: string | null) => void;
|
||||
isMultiSelectMode?: boolean;
|
||||
selectedHostIds?: Set<string>;
|
||||
toggleHostSelection?: (hostId: string) => void;
|
||||
}
|
||||
|
||||
const HostTreeItem: React.FC<HostTreeItemProps> = ({
|
||||
@@ -258,6 +276,9 @@ const HostTreeItem: React.FC<HostTreeItemProps> = ({
|
||||
onDeleteHost,
|
||||
onCopyCredentials,
|
||||
moveHostToGroup: _moveHostToGroup,
|
||||
isMultiSelectMode,
|
||||
selectedHostIds,
|
||||
toggleHostSelection,
|
||||
}) => {
|
||||
const { t } = useI18n();
|
||||
const paddingLeft = `${depth * 20 + 12}px`;
|
||||
@@ -270,18 +291,40 @@ const HostTreeItem: React.FC<HostTreeItemProps> = ({
|
||||
const displayPort = isTelnet
|
||||
? (host.telnetPort ?? host.port ?? 23)
|
||||
: (host.port ?? 22);
|
||||
const isSelected = isMultiSelectMode && selectedHostIds?.has(host.id);
|
||||
|
||||
return (
|
||||
<ContextMenu>
|
||||
<ContextMenuTrigger>
|
||||
<div
|
||||
className="flex items-center py-2 pr-3 text-sm cursor-pointer transition-colors select-none group hover:bg-secondary/40 rounded-lg"
|
||||
className={cn(
|
||||
"flex items-center py-2 pr-3 text-sm cursor-pointer transition-colors select-none group hover:bg-secondary/40 rounded-lg",
|
||||
isSelected ? "bg-primary/10" : "",
|
||||
)}
|
||||
style={{ paddingLeft }}
|
||||
draggable
|
||||
draggable={!isMultiSelectMode}
|
||||
onDragStart={(e) => e.dataTransfer.setData("host-id", host.id)}
|
||||
onClick={() => onConnect(safeHost)}
|
||||
onClick={() => {
|
||||
if (isMultiSelectMode && toggleHostSelection) {
|
||||
toggleHostSelection(host.id);
|
||||
} else {
|
||||
onConnect(safeHost);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<div className="mr-2 flex-shrink-0 w-4 h-4" />
|
||||
{isMultiSelectMode && (
|
||||
<div className="mr-2 flex-shrink-0" onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
toggleHostSelection?.(host.id);
|
||||
}}>
|
||||
{isSelected ? (
|
||||
<CheckSquare size={18} className="text-primary" />
|
||||
) : (
|
||||
<Square size={18} className="text-muted-foreground" />
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
{!isMultiSelectMode && <div className="mr-2 flex-shrink-0 w-4 h-4" />}
|
||||
<div className="mr-3 flex-shrink-0">
|
||||
<DistroAvatar host={host} fallback={(host.os || "L")[0].toUpperCase()} size="sm" />
|
||||
</div>
|
||||
@@ -351,6 +394,9 @@ export const HostTreeView: React.FC<HostTreeViewProps> = ({
|
||||
moveGroup,
|
||||
managedGroupPaths,
|
||||
onUnmanageGroup,
|
||||
isMultiSelectMode,
|
||||
selectedHostIds,
|
||||
toggleHostSelection,
|
||||
}) => {
|
||||
const { t } = useI18n();
|
||||
|
||||
@@ -471,6 +517,9 @@ export const HostTreeView: React.FC<HostTreeViewProps> = ({
|
||||
moveGroup={moveGroup}
|
||||
managedGroupPaths={managedGroupPaths}
|
||||
onUnmanageGroup={onUnmanageGroup}
|
||||
isMultiSelectMode={isMultiSelectMode}
|
||||
selectedHostIds={selectedHostIds}
|
||||
toggleHostSelection={toggleHostSelection}
|
||||
/>
|
||||
))}
|
||||
|
||||
@@ -486,6 +535,9 @@ export const HostTreeView: React.FC<HostTreeViewProps> = ({
|
||||
onDeleteHost={onDeleteHost}
|
||||
onCopyCredentials={onCopyCredentials}
|
||||
moveHostToGroup={moveHostToGroup}
|
||||
isMultiSelectMode={isMultiSelectMode}
|
||||
selectedHostIds={selectedHostIds}
|
||||
toggleHostSelection={toggleHostSelection}
|
||||
/>
|
||||
))}
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@ import { I18nProvider } from "../application/i18n/I18nProvider";
|
||||
import { useSettingsState } from "../application/state/useSettingsState";
|
||||
import { useTrayPanelBackend } from "../application/state/useTrayPanelBackend";
|
||||
import { useActiveTabId } from "../application/state/activeTabStore";
|
||||
import { X, Maximize2, ChevronRight, ChevronDown } from "lucide-react";
|
||||
import { X, Maximize2, ChevronRight, ChevronDown, Power } from "lucide-react";
|
||||
import { AppLogo } from "./AppLogo";
|
||||
|
||||
const StatusDot: React.FC<{ status: "success" | "warning" | "error" | "neutral"; spinning?: boolean }> = ({
|
||||
@@ -109,6 +109,7 @@ const TrayPanelContent: React.FC = () => {
|
||||
const {
|
||||
hideTrayPanel,
|
||||
openMainWindow,
|
||||
quitApp,
|
||||
jumpToSession,
|
||||
onTrayPanelCloseRequest,
|
||||
onTrayPanelRefresh,
|
||||
@@ -200,8 +201,12 @@ const TrayPanelContent: React.FC = () => {
|
||||
void openMainWindow();
|
||||
}, [openMainWindow]);
|
||||
|
||||
const handleQuit = useCallback(() => {
|
||||
void quitApp();
|
||||
}, [quitApp]);
|
||||
|
||||
return (
|
||||
<div id="tray-panel-root" className="w-full h-full bg-background/95 backdrop-blur border border-border/60 rounded-lg shadow-lg overflow-hidden">
|
||||
<div id="tray-panel-root" className="w-full h-full bg-background/95 backdrop-blur border border-border/60 rounded-lg shadow-lg overflow-hidden flex flex-col">
|
||||
<div className="px-3 py-2 border-b border-border/60 flex items-center justify-between app-no-drag">
|
||||
<div className="flex items-center gap-2">
|
||||
<AppLogo className="w-5 h-5" />
|
||||
@@ -225,7 +230,7 @@ const TrayPanelContent: React.FC = () => {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="p-2 space-y-3 text-sm">
|
||||
<div className="p-2 space-y-3 text-sm flex-1 overflow-y-auto min-h-0">
|
||||
|
||||
{jumpableSessions.length > 0 && (() => {
|
||||
// Group sessions by workspace
|
||||
@@ -378,6 +383,17 @@ const TrayPanelContent: React.FC = () => {
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Quit button at the bottom */}
|
||||
<div className="px-3 py-2 border-t border-border/60">
|
||||
<button
|
||||
className="w-full text-left px-2 py-1.5 rounded hover:bg-destructive/10 flex items-center gap-2 text-sm text-muted-foreground hover:text-destructive transition-colors"
|
||||
onClick={handleQuit}
|
||||
>
|
||||
<Power size={14} />
|
||||
<span>{t("tray.quit")}</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1857,6 +1857,9 @@ const VaultViewInner: React.FC<VaultViewProps> = ({
|
||||
moveGroup={moveGroup}
|
||||
managedGroupPaths={managedGroupPaths}
|
||||
onUnmanageGroup={handleUnmanageGroup}
|
||||
isMultiSelectMode={isMultiSelectMode}
|
||||
selectedHostIds={selectedHostIds}
|
||||
toggleHostSelection={toggleHostSelection}
|
||||
/>
|
||||
) : sortMode === "group" && groupedDisplayHosts ? (
|
||||
<div className="space-y-6">
|
||||
|
||||
@@ -691,6 +691,13 @@ function registerHandlers(ipcMain) {
|
||||
return { success: true };
|
||||
});
|
||||
|
||||
ipcMain.handle("netcatty:trayPanel:quitApp", async () => {
|
||||
const { app } = electronModule;
|
||||
closeToTray = false;
|
||||
app.quit();
|
||||
return { success: true };
|
||||
});
|
||||
|
||||
console.log("[GlobalShortcut] IPC handlers registered");
|
||||
}
|
||||
|
||||
@@ -700,6 +707,20 @@ function registerHandlers(ipcMain) {
|
||||
function cleanup() {
|
||||
unregisterGlobalHotkey();
|
||||
destroyTray();
|
||||
|
||||
if (trayPanelRefreshTimer) {
|
||||
clearInterval(trayPanelRefreshTimer);
|
||||
trayPanelRefreshTimer = null;
|
||||
}
|
||||
|
||||
if (trayPanelWindow && !trayPanelWindow.isDestroyed()) {
|
||||
try {
|
||||
trayPanelWindow.destroy();
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
trayPanelWindow = null;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
|
||||
@@ -241,6 +241,15 @@ function focusMainWindow() {
|
||||
const win = wins && wins.length ? wins[0] : null;
|
||||
if (!win) return false;
|
||||
|
||||
// Check if the webContents has crashed or been destroyed
|
||||
try {
|
||||
if (win.webContents?.isCrashed?.()) {
|
||||
console.warn('[Main] Main window webContents has crashed, destroying window');
|
||||
win.destroy();
|
||||
return false;
|
||||
}
|
||||
} catch {}
|
||||
|
||||
try {
|
||||
if (win.isMinimized && win.isMinimized()) win.restore();
|
||||
} catch {}
|
||||
@@ -685,100 +694,114 @@ function showStartupError(err) {
|
||||
}
|
||||
}
|
||||
|
||||
// Application lifecycle
|
||||
app.whenReady().then(() => {
|
||||
registerAppProtocol();
|
||||
|
||||
// Set dock icon on macOS
|
||||
if (isMac && appIcon && app.dock?.setIcon) {
|
||||
try {
|
||||
app.dock.setIcon(appIcon);
|
||||
} catch (err) {
|
||||
console.warn("Failed to set dock icon", err);
|
||||
}
|
||||
}
|
||||
|
||||
// Build and set application menu
|
||||
const menu = windowManager.buildAppMenu(Menu, app, isMac);
|
||||
Menu.setApplicationMenu(menu);
|
||||
|
||||
app.on("browser-window-created", (_event, win) => {
|
||||
try {
|
||||
const mainWin = windowManager.getMainWindow();
|
||||
const settingsWin = windowManager.getSettingsWindow();
|
||||
const isPrimary = win === mainWin || win === settingsWin;
|
||||
if (!isPrimary) {
|
||||
win.setMenuBarVisibility(false);
|
||||
win.autoHideMenuBar = true;
|
||||
win.setMenu(null);
|
||||
if (appIcon && win.setIcon) win.setIcon(appIcon);
|
||||
}
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
});
|
||||
|
||||
// Create the main window
|
||||
void createWindow().catch((err) => {
|
||||
console.error("[Main] Failed to create main window:", err);
|
||||
showStartupError(err);
|
||||
try {
|
||||
app.quit();
|
||||
} catch {}
|
||||
});
|
||||
|
||||
// Re-create window on macOS dock click
|
||||
app.on("activate", () => {
|
||||
if (BrowserWindow.getAllWindows().length === 0) {
|
||||
// Ensure single-instance behavior — must run before app.whenReady() so
|
||||
// the second instance never attempts to register the app:// protocol or
|
||||
// create a BrowserWindow (which would fail with ERR_FAILED).
|
||||
const gotLock = app.requestSingleInstanceLock();
|
||||
if (!gotLock) {
|
||||
app.quit();
|
||||
} else {
|
||||
app.on("second-instance", () => {
|
||||
if (!focusMainWindow()) {
|
||||
// Window is missing or crashed — try to recreate it
|
||||
void createWindow().catch((err) => {
|
||||
console.error("[Main] Failed to create window on activate:", err);
|
||||
console.error("[Main] Failed to recreate window on second-instance:", err);
|
||||
showStartupError(err);
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Ensure single-instance behavior focuses existing window
|
||||
try {
|
||||
const gotLock = app.requestSingleInstanceLock();
|
||||
if (!gotLock) {
|
||||
app.quit();
|
||||
} else {
|
||||
app.on("second-instance", () => {
|
||||
focusMainWindow();
|
||||
// Application lifecycle
|
||||
app.whenReady().then(() => {
|
||||
registerAppProtocol();
|
||||
|
||||
// Set dock icon on macOS
|
||||
if (isMac && appIcon && app.dock?.setIcon) {
|
||||
try {
|
||||
app.dock.setIcon(appIcon);
|
||||
} catch (err) {
|
||||
console.warn("Failed to set dock icon", err);
|
||||
}
|
||||
}
|
||||
|
||||
// Build and set application menu
|
||||
const menu = windowManager.buildAppMenu(Menu, app, isMac);
|
||||
Menu.setApplicationMenu(menu);
|
||||
|
||||
app.on("browser-window-created", (_event, win) => {
|
||||
try {
|
||||
const mainWin = windowManager.getMainWindow();
|
||||
const settingsWin = windowManager.getSettingsWindow();
|
||||
const isPrimary = win === mainWin || win === settingsWin;
|
||||
if (!isPrimary) {
|
||||
win.setMenuBarVisibility(false);
|
||||
win.autoHideMenuBar = true;
|
||||
win.setMenu(null);
|
||||
if (appIcon && win.setIcon) win.setIcon(appIcon);
|
||||
}
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
});
|
||||
}
|
||||
} catch {}
|
||||
|
||||
// Cleanup on all windows closed
|
||||
app.on("window-all-closed", () => {
|
||||
if (process.platform !== "darwin") {
|
||||
// Create the main window
|
||||
void createWindow().catch((err) => {
|
||||
console.error("[Main] Failed to create main window:", err);
|
||||
showStartupError(err);
|
||||
try {
|
||||
app.quit();
|
||||
} catch {}
|
||||
});
|
||||
|
||||
// Re-create window on macOS dock click
|
||||
app.on("activate", () => {
|
||||
if (BrowserWindow.getAllWindows().length === 0) {
|
||||
void createWindow().catch((err) => {
|
||||
console.error("[Main] Failed to create window on activate:", err);
|
||||
showStartupError(err);
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Cleanup on all windows closed
|
||||
app.on("window-all-closed", () => {
|
||||
if (process.platform !== "darwin") {
|
||||
app.quit();
|
||||
}
|
||||
});
|
||||
|
||||
app.on("before-quit", () => {
|
||||
windowManager.setIsQuitting(true);
|
||||
});
|
||||
|
||||
// Cleanup all PTY sessions and port forwarding tunnels before quitting
|
||||
app.on("will-quit", () => {
|
||||
try {
|
||||
terminalBridge.cleanupAllSessions();
|
||||
} catch (err) {
|
||||
console.warn("Error during terminal cleanup:", err);
|
||||
}
|
||||
try {
|
||||
portForwardingBridge.stopAllPortForwards();
|
||||
} catch (err) {
|
||||
console.warn("Error during port forwarding cleanup:", err);
|
||||
}
|
||||
try {
|
||||
globalShortcutBridge.cleanup();
|
||||
} catch (err) {
|
||||
console.warn("Error during global shortcut cleanup:", err);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Graceful shutdown on SIGTERM/SIGINT to prevent zombie processes
|
||||
for (const sig of ['SIGTERM', 'SIGINT']) {
|
||||
process.on(sig, () => {
|
||||
console.log(`[Main] Received ${sig}, quitting…`);
|
||||
app.quit();
|
||||
}
|
||||
});
|
||||
|
||||
app.on("before-quit", () => {
|
||||
windowManager.setIsQuitting(true);
|
||||
});
|
||||
|
||||
// Cleanup all PTY sessions and port forwarding tunnels before quitting
|
||||
app.on("will-quit", () => {
|
||||
try {
|
||||
terminalBridge.cleanupAllSessions();
|
||||
} catch (err) {
|
||||
console.warn("Error during terminal cleanup:", err);
|
||||
}
|
||||
try {
|
||||
portForwardingBridge.stopAllPortForwards();
|
||||
} catch (err) {
|
||||
console.warn("Error during port forwarding cleanup:", err);
|
||||
}
|
||||
try {
|
||||
globalShortcutBridge.cleanup();
|
||||
} catch (err) {
|
||||
console.warn("Error during global shortcut cleanup:", err);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Export for testing
|
||||
module.exports = {
|
||||
|
||||
@@ -801,6 +801,7 @@ const api = {
|
||||
// Tray panel window
|
||||
hideTrayPanel: () => ipcRenderer.invoke("netcatty:trayPanel:hide"),
|
||||
openMainWindow: () => ipcRenderer.invoke("netcatty:trayPanel:openMainWindow"),
|
||||
quitApp: () => ipcRenderer.invoke("netcatty:trayPanel:quitApp"),
|
||||
jumpToSessionFromTrayPanel: (sessionId) =>
|
||||
ipcRenderer.invoke("netcatty:trayPanel:jumpToSession", sessionId),
|
||||
connectToHostFromTrayPanel: (hostId) =>
|
||||
|
||||
1
global.d.ts
vendored
1
global.d.ts
vendored
@@ -610,6 +610,7 @@ declare global {
|
||||
|
||||
hideTrayPanel?(): Promise<{ success: boolean }>;
|
||||
openMainWindow?(): Promise<{ success: boolean }>;
|
||||
quitApp?(): Promise<{ success: boolean }>;
|
||||
jumpToSessionFromTrayPanel?(sessionId: string): Promise<{ success: boolean }>;
|
||||
connectToHostFromTrayPanel?(hostId: string): Promise<{ success: boolean }>;
|
||||
onTrayPanelCloseRequest?(callback: () => void): () => void;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
diff --git a/node_modules/ssh2/lib/protocol/SFTP.js b/node_modules/ssh2/lib/protocol/SFTP.js
|
||||
index 9f33c02..c311d3a 100644
|
||||
index 9f33c02..9751164 100644
|
||||
--- a/node_modules/ssh2/lib/protocol/SFTP.js
|
||||
+++ b/node_modules/ssh2/lib/protocol/SFTP.js
|
||||
@@ -117,6 +117,20 @@ const OPENSSH_MAX_PKT_LEN = 256 * 1024;
|
||||
@@ -23,7 +23,70 @@ index 9f33c02..c311d3a 100644
|
||||
const fakeStderr = {
|
||||
readable: false,
|
||||
writable: false,
|
||||
@@ -339,7 +351,7 @@ class SFTP extends EventEmitter {
|
||||
@@ -155,6 +169,8 @@ class SFTP extends EventEmitter {
|
||||
this._writeReqid = -1;
|
||||
this._requests = {};
|
||||
this._maxInPktLen = OPENSSH_MAX_PKT_LEN;
|
||||
+ this._preambleSkipped = false; // Track if we've found the start of SFTP binary data
|
||||
+ this._preambleBuf = null; // Buffer for partial preamble data across frames
|
||||
this._maxOutPktLen = 34000;
|
||||
this._maxReadLen =
|
||||
(this._isOpenSSH ? OPENSSH_MAX_PKT_LEN : 34000) - PKT_RW_OVERHEAD;
|
||||
@@ -196,6 +212,53 @@ class SFTP extends EventEmitter {
|
||||
this.emit('end');
|
||||
return;
|
||||
}
|
||||
+
|
||||
+ // Skip non-SFTP preamble data (e.g. MOTD/banner text from misconfigured servers)
|
||||
+ // Only applies to client mode; server mode expects SSH_FXP_INIT directly.
|
||||
+ if (!this._preambleSkipped) {
|
||||
+ if (this.server) {
|
||||
+ // Server mode: no preamble skipping, proceed to normal parsing
|
||||
+ this._preambleSkipped = true;
|
||||
+ } else {
|
||||
+ // Concatenate with any previously buffered partial data
|
||||
+ if (this._preambleBuf) {
|
||||
+ data = Buffer.concat([this._preambleBuf, data]);
|
||||
+ this._preambleBuf = null;
|
||||
+ }
|
||||
+
|
||||
+ // Look for the start of a valid SFTP packet in the data.
|
||||
+ // The first SFTP packet from the server is SSH_FXP_VERSION (type=2).
|
||||
+ // Format: uint32 length, byte type=0x02, uint32 version, ...
|
||||
+ // The length should be >= 5 (1 byte type + 4 bytes version).
|
||||
+ let found = -1;
|
||||
+ for (let i = 0; i <= data.length - 5; i++) {
|
||||
+ const len = (data[i] << 24) | (data[i+1] << 16) | (data[i+2] << 8) | data[i+3];
|
||||
+ if (len >= 5 && len <= this._maxInPktLen && data[i+4] === 0x02) {
|
||||
+ found = i;
|
||||
+ break;
|
||||
+ }
|
||||
+ }
|
||||
+ if (found === -1) {
|
||||
+ // No valid SFTP packet header found yet.
|
||||
+ // Keep up to the last 4 bytes in case a valid header spans this and the
|
||||
+ // next chunk (the uint32 length could be split across frames).
|
||||
+ const keep = Math.min(data.length, 4);
|
||||
+ this._preambleBuf = Buffer.from(data.slice(data.length - keep));
|
||||
+ this._debug && this._debug(
|
||||
+ 'SFTP: Skipping non-SFTP preamble data (' + data.length + ' bytes, buffered last ' + keep + ')'
|
||||
+ );
|
||||
+ return;
|
||||
+ }
|
||||
+ if (found > 0) {
|
||||
+ this._debug && this._debug(
|
||||
+ 'SFTP: Skipped ' + found + ' bytes of non-SFTP preamble data'
|
||||
+ );
|
||||
+ data = data.slice(found);
|
||||
+ }
|
||||
+ this._preambleSkipped = true;
|
||||
+ }
|
||||
+ }
|
||||
+
|
||||
/*
|
||||
uint32 length
|
||||
byte type
|
||||
@@ -339,7 +402,7 @@ class SFTP extends EventEmitter {
|
||||
uint32 pflags
|
||||
ATTRS attrs
|
||||
*/
|
||||
@@ -32,7 +95,7 @@ index 9f33c02..c311d3a 100644
|
||||
let p = 9;
|
||||
const buf = Buffer.allocUnsafe(4 + 1 + 4 + 4 + pathLen + 4 + 4 + attrsLen);
|
||||
|
||||
@@ -349,7 +361,7 @@ class SFTP extends EventEmitter {
|
||||
@@ -349,7 +412,7 @@ class SFTP extends EventEmitter {
|
||||
writeUInt32BE(buf, reqid, 5);
|
||||
|
||||
writeUInt32BE(buf, pathLen, p);
|
||||
@@ -41,7 +104,7 @@ index 9f33c02..c311d3a 100644
|
||||
writeUInt32BE(buf, flags, p += pathLen);
|
||||
writeUInt32BE(buf, attrsFlags, p += 4);
|
||||
if (attrsLen) {
|
||||
@@ -734,7 +746,7 @@ class SFTP extends EventEmitter {
|
||||
@@ -734,7 +797,7 @@ class SFTP extends EventEmitter {
|
||||
uint32 id
|
||||
string filename
|
||||
*/
|
||||
@@ -50,7 +113,7 @@ index 9f33c02..c311d3a 100644
|
||||
let p = 9;
|
||||
const buf = Buffer.allocUnsafe(4 + 1 + 4 + 4 + fnameLen);
|
||||
|
||||
@@ -744,7 +756,7 @@ class SFTP extends EventEmitter {
|
||||
@@ -744,7 +807,7 @@ class SFTP extends EventEmitter {
|
||||
writeUInt32BE(buf, reqid, 5);
|
||||
|
||||
writeUInt32BE(buf, fnameLen, p);
|
||||
@@ -59,7 +122,7 @@ index 9f33c02..c311d3a 100644
|
||||
|
||||
this._requests[reqid] = { cb };
|
||||
|
||||
@@ -762,8 +774,8 @@ class SFTP extends EventEmitter {
|
||||
@@ -762,8 +825,8 @@ class SFTP extends EventEmitter {
|
||||
string oldpath
|
||||
string newpath
|
||||
*/
|
||||
@@ -70,7 +133,7 @@ index 9f33c02..c311d3a 100644
|
||||
let p = 9;
|
||||
const buf = Buffer.allocUnsafe(4 + 1 + 4 + 4 + oldLen + 4 + newLen);
|
||||
|
||||
@@ -773,9 +785,9 @@ class SFTP extends EventEmitter {
|
||||
@@ -773,9 +836,9 @@ class SFTP extends EventEmitter {
|
||||
writeUInt32BE(buf, reqid, 5);
|
||||
|
||||
writeUInt32BE(buf, oldLen, p);
|
||||
@@ -82,7 +145,7 @@ index 9f33c02..c311d3a 100644
|
||||
|
||||
this._requests[reqid] = { cb };
|
||||
|
||||
@@ -806,7 +818,7 @@ class SFTP extends EventEmitter {
|
||||
@@ -806,7 +869,7 @@ class SFTP extends EventEmitter {
|
||||
string path
|
||||
ATTRS attrs
|
||||
*/
|
||||
@@ -91,7 +154,7 @@ index 9f33c02..c311d3a 100644
|
||||
let p = 9;
|
||||
const buf = Buffer.allocUnsafe(4 + 1 + 4 + 4 + pathLen + 4 + attrsLen);
|
||||
|
||||
@@ -816,7 +828,7 @@ class SFTP extends EventEmitter {
|
||||
@@ -816,7 +879,7 @@ class SFTP extends EventEmitter {
|
||||
writeUInt32BE(buf, reqid, 5);
|
||||
|
||||
writeUInt32BE(buf, pathLen, p);
|
||||
@@ -100,7 +163,7 @@ index 9f33c02..c311d3a 100644
|
||||
writeUInt32BE(buf, flags, p += pathLen);
|
||||
if (attrsLen) {
|
||||
p += 4;
|
||||
@@ -844,7 +856,7 @@ class SFTP extends EventEmitter {
|
||||
@@ -844,7 +907,7 @@ class SFTP extends EventEmitter {
|
||||
uint32 id
|
||||
string path
|
||||
*/
|
||||
@@ -109,7 +172,7 @@ index 9f33c02..c311d3a 100644
|
||||
let p = 9;
|
||||
const buf = Buffer.allocUnsafe(4 + 1 + 4 + 4 + pathLen);
|
||||
|
||||
@@ -854,7 +866,7 @@ class SFTP extends EventEmitter {
|
||||
@@ -854,7 +917,7 @@ class SFTP extends EventEmitter {
|
||||
writeUInt32BE(buf, reqid, 5);
|
||||
|
||||
writeUInt32BE(buf, pathLen, p);
|
||||
@@ -118,7 +181,7 @@ index 9f33c02..c311d3a 100644
|
||||
|
||||
this._requests[reqid] = { cb };
|
||||
|
||||
@@ -987,7 +999,7 @@ class SFTP extends EventEmitter {
|
||||
@@ -987,7 +1050,7 @@ class SFTP extends EventEmitter {
|
||||
uint32 id
|
||||
string path
|
||||
*/
|
||||
@@ -127,7 +190,7 @@ index 9f33c02..c311d3a 100644
|
||||
let p = 9;
|
||||
const buf = Buffer.allocUnsafe(4 + 1 + 4 + 4 + pathLen);
|
||||
|
||||
@@ -997,7 +1009,7 @@ class SFTP extends EventEmitter {
|
||||
@@ -997,7 +1060,7 @@ class SFTP extends EventEmitter {
|
||||
writeUInt32BE(buf, reqid, 5);
|
||||
|
||||
writeUInt32BE(buf, pathLen, p);
|
||||
@@ -136,7 +199,7 @@ index 9f33c02..c311d3a 100644
|
||||
|
||||
this._requests[reqid] = { cb };
|
||||
|
||||
@@ -1014,7 +1026,7 @@ class SFTP extends EventEmitter {
|
||||
@@ -1014,7 +1077,7 @@ class SFTP extends EventEmitter {
|
||||
uint32 id
|
||||
string path
|
||||
*/
|
||||
@@ -145,7 +208,7 @@ index 9f33c02..c311d3a 100644
|
||||
let p = 9;
|
||||
const buf = Buffer.allocUnsafe(4 + 1 + 4 + 4 + pathLen);
|
||||
|
||||
@@ -1024,7 +1036,7 @@ class SFTP extends EventEmitter {
|
||||
@@ -1024,7 +1087,7 @@ class SFTP extends EventEmitter {
|
||||
writeUInt32BE(buf, reqid, 5);
|
||||
|
||||
writeUInt32BE(buf, pathLen, p);
|
||||
@@ -154,7 +217,7 @@ index 9f33c02..c311d3a 100644
|
||||
|
||||
this._requests[reqid] = { cb };
|
||||
|
||||
@@ -1041,7 +1053,7 @@ class SFTP extends EventEmitter {
|
||||
@@ -1041,7 +1104,7 @@ class SFTP extends EventEmitter {
|
||||
uint32 id
|
||||
string path
|
||||
*/
|
||||
@@ -163,7 +226,7 @@ index 9f33c02..c311d3a 100644
|
||||
let p = 9;
|
||||
const buf = Buffer.allocUnsafe(4 + 1 + 4 + 4 + pathLen);
|
||||
|
||||
@@ -1051,7 +1063,7 @@ class SFTP extends EventEmitter {
|
||||
@@ -1051,7 +1114,7 @@ class SFTP extends EventEmitter {
|
||||
writeUInt32BE(buf, reqid, 5);
|
||||
|
||||
writeUInt32BE(buf, pathLen, p);
|
||||
@@ -172,7 +235,7 @@ index 9f33c02..c311d3a 100644
|
||||
|
||||
this._requests[reqid] = { cb };
|
||||
|
||||
@@ -1080,7 +1092,7 @@ class SFTP extends EventEmitter {
|
||||
@@ -1080,7 +1143,7 @@ class SFTP extends EventEmitter {
|
||||
string path
|
||||
ATTRS attrs
|
||||
*/
|
||||
@@ -181,7 +244,7 @@ index 9f33c02..c311d3a 100644
|
||||
let p = 9;
|
||||
const buf = Buffer.allocUnsafe(4 + 1 + 4 + 4 + pathLen + 4 + attrsLen);
|
||||
|
||||
@@ -1090,7 +1102,7 @@ class SFTP extends EventEmitter {
|
||||
@@ -1090,7 +1153,7 @@ class SFTP extends EventEmitter {
|
||||
writeUInt32BE(buf, reqid, 5);
|
||||
|
||||
writeUInt32BE(buf, pathLen, p);
|
||||
@@ -190,7 +253,7 @@ index 9f33c02..c311d3a 100644
|
||||
writeUInt32BE(buf, flags, p += pathLen);
|
||||
if (attrsLen) {
|
||||
p += 4;
|
||||
@@ -1205,7 +1217,7 @@ class SFTP extends EventEmitter {
|
||||
@@ -1205,7 +1268,7 @@ class SFTP extends EventEmitter {
|
||||
uint32 id
|
||||
string path
|
||||
*/
|
||||
@@ -199,7 +262,7 @@ index 9f33c02..c311d3a 100644
|
||||
let p = 9;
|
||||
const buf = Buffer.allocUnsafe(4 + 1 + 4 + 4 + pathLen);
|
||||
|
||||
@@ -1215,7 +1227,7 @@ class SFTP extends EventEmitter {
|
||||
@@ -1215,7 +1278,7 @@ class SFTP extends EventEmitter {
|
||||
writeUInt32BE(buf, reqid, 5);
|
||||
|
||||
writeUInt32BE(buf, pathLen, p);
|
||||
@@ -208,7 +271,7 @@ index 9f33c02..c311d3a 100644
|
||||
|
||||
this._requests[reqid] = {
|
||||
cb: (err, names) => {
|
||||
@@ -1243,8 +1255,8 @@ class SFTP extends EventEmitter {
|
||||
@@ -1243,8 +1306,8 @@ class SFTP extends EventEmitter {
|
||||
string linkpath
|
||||
string targetpath
|
||||
*/
|
||||
@@ -219,7 +282,7 @@ index 9f33c02..c311d3a 100644
|
||||
let p = 9;
|
||||
const buf = Buffer.allocUnsafe(4 + 1 + 4 + 4 + linkLen + 4 + targetLen);
|
||||
|
||||
@@ -1256,14 +1268,14 @@ class SFTP extends EventEmitter {
|
||||
@@ -1256,14 +1319,14 @@ class SFTP extends EventEmitter {
|
||||
if (this._isOpenSSH) {
|
||||
// OpenSSH has linkpath and targetpath positions switched
|
||||
writeUInt32BE(buf, targetLen, p);
|
||||
@@ -238,7 +301,7 @@ index 9f33c02..c311d3a 100644
|
||||
}
|
||||
|
||||
this._requests[reqid] = { cb };
|
||||
@@ -1281,7 +1293,7 @@ class SFTP extends EventEmitter {
|
||||
@@ -1281,7 +1344,7 @@ class SFTP extends EventEmitter {
|
||||
uint32 id
|
||||
string path
|
||||
*/
|
||||
@@ -247,7 +310,7 @@ index 9f33c02..c311d3a 100644
|
||||
let p = 9;
|
||||
const buf = Buffer.allocUnsafe(4 + 1 + 4 + 4 + pathLen);
|
||||
|
||||
@@ -1291,7 +1303,7 @@ class SFTP extends EventEmitter {
|
||||
@@ -1291,7 +1354,7 @@ class SFTP extends EventEmitter {
|
||||
writeUInt32BE(buf, reqid, 5);
|
||||
|
||||
writeUInt32BE(buf, pathLen, p);
|
||||
@@ -256,7 +319,7 @@ index 9f33c02..c311d3a 100644
|
||||
|
||||
this._requests[reqid] = {
|
||||
cb: (err, names) => {
|
||||
@@ -1325,8 +1337,8 @@ class SFTP extends EventEmitter {
|
||||
@@ -1325,8 +1388,8 @@ class SFTP extends EventEmitter {
|
||||
string oldpath
|
||||
string newpath
|
||||
*/
|
||||
@@ -267,7 +330,7 @@ index 9f33c02..c311d3a 100644
|
||||
let p = 9;
|
||||
const buf =
|
||||
Buffer.allocUnsafe(4 + 1 + 4 + 4 + 24 + 4 + oldLen + 4 + newLen);
|
||||
@@ -1337,11 +1349,11 @@ class SFTP extends EventEmitter {
|
||||
@@ -1337,11 +1400,11 @@ class SFTP extends EventEmitter {
|
||||
writeUInt32BE(buf, reqid, 5);
|
||||
|
||||
writeUInt32BE(buf, 24, p);
|
||||
@@ -282,7 +345,7 @@ index 9f33c02..c311d3a 100644
|
||||
|
||||
this._requests[reqid] = { cb };
|
||||
|
||||
@@ -1364,7 +1376,7 @@ class SFTP extends EventEmitter {
|
||||
@@ -1364,7 +1427,7 @@ class SFTP extends EventEmitter {
|
||||
string "statvfs@openssh.com"
|
||||
string path
|
||||
*/
|
||||
@@ -291,7 +354,7 @@ index 9f33c02..c311d3a 100644
|
||||
let p = 9;
|
||||
const buf = Buffer.allocUnsafe(4 + 1 + 4 + 4 + 19 + 4 + pathLen);
|
||||
|
||||
@@ -1374,9 +1386,9 @@ class SFTP extends EventEmitter {
|
||||
@@ -1374,9 +1437,9 @@ class SFTP extends EventEmitter {
|
||||
writeUInt32BE(buf, reqid, 5);
|
||||
|
||||
writeUInt32BE(buf, 19, p);
|
||||
@@ -303,7 +366,7 @@ index 9f33c02..c311d3a 100644
|
||||
|
||||
this._requests[reqid] = { extended: 'statvfs@openssh.com', cb };
|
||||
|
||||
@@ -1411,7 +1423,7 @@ class SFTP extends EventEmitter {
|
||||
@@ -1411,7 +1474,7 @@ class SFTP extends EventEmitter {
|
||||
writeUInt32BE(buf, reqid, 5);
|
||||
|
||||
writeUInt32BE(buf, 20, p);
|
||||
@@ -312,7 +375,7 @@ index 9f33c02..c311d3a 100644
|
||||
writeUInt32BE(buf, handleLen, p += 20);
|
||||
buf.set(handle, p += 4);
|
||||
|
||||
@@ -1437,8 +1449,8 @@ class SFTP extends EventEmitter {
|
||||
@@ -1437,8 +1500,8 @@ class SFTP extends EventEmitter {
|
||||
string oldpath
|
||||
string newpath
|
||||
*/
|
||||
@@ -323,7 +386,7 @@ index 9f33c02..c311d3a 100644
|
||||
let p = 9;
|
||||
const buf =
|
||||
Buffer.allocUnsafe(4 + 1 + 4 + 4 + 20 + 4 + oldLen + 4 + newLen);
|
||||
@@ -1449,11 +1461,11 @@ class SFTP extends EventEmitter {
|
||||
@@ -1449,11 +1512,11 @@ class SFTP extends EventEmitter {
|
||||
writeUInt32BE(buf, reqid, 5);
|
||||
|
||||
writeUInt32BE(buf, 20, p);
|
||||
@@ -338,7 +401,7 @@ index 9f33c02..c311d3a 100644
|
||||
|
||||
this._requests[reqid] = { cb };
|
||||
|
||||
@@ -1488,7 +1500,7 @@ class SFTP extends EventEmitter {
|
||||
@@ -1488,7 +1551,7 @@ class SFTP extends EventEmitter {
|
||||
writeUInt32BE(buf, reqid, 5);
|
||||
|
||||
writeUInt32BE(buf, 17, p);
|
||||
@@ -347,7 +410,7 @@ index 9f33c02..c311d3a 100644
|
||||
writeUInt32BE(buf, handleLen, p += 17);
|
||||
buf.set(handle, p += 4);
|
||||
|
||||
@@ -1524,7 +1536,7 @@ class SFTP extends EventEmitter {
|
||||
@@ -1524,7 +1587,7 @@ class SFTP extends EventEmitter {
|
||||
string path
|
||||
ATTRS attrs
|
||||
*/
|
||||
@@ -356,7 +419,7 @@ index 9f33c02..c311d3a 100644
|
||||
let p = 9;
|
||||
const buf =
|
||||
Buffer.allocUnsafe(4 + 1 + 4 + 4 + 20 + 4 + pathLen + 4 + attrsLen);
|
||||
@@ -1535,10 +1547,10 @@ class SFTP extends EventEmitter {
|
||||
@@ -1535,10 +1598,10 @@ class SFTP extends EventEmitter {
|
||||
writeUInt32BE(buf, reqid, 5);
|
||||
|
||||
writeUInt32BE(buf, 20, p);
|
||||
@@ -369,7 +432,7 @@ index 9f33c02..c311d3a 100644
|
||||
|
||||
writeUInt32BE(buf, flags, p += pathLen);
|
||||
if (attrsLen) {
|
||||
@@ -1573,7 +1585,7 @@ class SFTP extends EventEmitter {
|
||||
@@ -1573,7 +1636,7 @@ class SFTP extends EventEmitter {
|
||||
string "expand-path@openssh.com"
|
||||
string path
|
||||
*/
|
||||
@@ -378,7 +441,7 @@ index 9f33c02..c311d3a 100644
|
||||
let p = 9;
|
||||
const buf = Buffer.allocUnsafe(4 + 1 + 4 + 4 + 23 + 4 + pathLen);
|
||||
|
||||
@@ -1583,10 +1595,10 @@ class SFTP extends EventEmitter {
|
||||
@@ -1583,10 +1646,10 @@ class SFTP extends EventEmitter {
|
||||
writeUInt32BE(buf, reqid, 5);
|
||||
|
||||
writeUInt32BE(buf, 23, p);
|
||||
@@ -391,7 +454,7 @@ index 9f33c02..c311d3a 100644
|
||||
|
||||
this._requests[reqid] = {
|
||||
cb: (err, names) => {
|
||||
@@ -1653,7 +1665,7 @@ class SFTP extends EventEmitter {
|
||||
@@ -1653,7 +1716,7 @@ class SFTP extends EventEmitter {
|
||||
|
||||
writeUInt32BE(buf, 9, p);
|
||||
p += 4;
|
||||
@@ -400,7 +463,7 @@ index 9f33c02..c311d3a 100644
|
||||
p += 9;
|
||||
|
||||
writeUInt32BE(buf, srcHandle.length, p);
|
||||
@@ -1708,7 +1720,7 @@ class SFTP extends EventEmitter {
|
||||
@@ -1708,7 +1771,7 @@ class SFTP extends EventEmitter {
|
||||
string username
|
||||
*/
|
||||
let p = 0;
|
||||
@@ -409,7 +472,7 @@ index 9f33c02..c311d3a 100644
|
||||
const buf = Buffer.allocUnsafe(
|
||||
4 + 1
|
||||
+ 4
|
||||
@@ -1728,12 +1740,12 @@ class SFTP extends EventEmitter {
|
||||
@@ -1728,12 +1791,12 @@ class SFTP extends EventEmitter {
|
||||
|
||||
writeUInt32BE(buf, 14, p);
|
||||
p += 4;
|
||||
@@ -424,7 +487,7 @@ index 9f33c02..c311d3a 100644
|
||||
p += usernameLen;
|
||||
|
||||
this._requests[reqid] = {
|
||||
@@ -1806,7 +1818,7 @@ class SFTP extends EventEmitter {
|
||||
@@ -1806,7 +1869,7 @@ class SFTP extends EventEmitter {
|
||||
|
||||
writeUInt32BE(buf, 30, p);
|
||||
p += 4;
|
||||
@@ -433,7 +496,7 @@ index 9f33c02..c311d3a 100644
|
||||
p += 30;
|
||||
|
||||
writeUInt32BE(buf, 4 * uids.length, p);
|
||||
@@ -1871,7 +1883,7 @@ class SFTP extends EventEmitter {
|
||||
@@ -1871,7 +1934,7 @@ class SFTP extends EventEmitter {
|
||||
|
||||
message || (message = '');
|
||||
|
||||
@@ -442,7 +505,7 @@ index 9f33c02..c311d3a 100644
|
||||
let p = 9;
|
||||
const buf = Buffer.allocUnsafe(4 + 1 + 4 + 4 + 4 + msgLen + 4);
|
||||
|
||||
@@ -1884,7 +1896,7 @@ class SFTP extends EventEmitter {
|
||||
@@ -1884,7 +1947,7 @@ class SFTP extends EventEmitter {
|
||||
writeUInt32BE(buf, msgLen, p += 4);
|
||||
p += 4;
|
||||
if (msgLen) {
|
||||
@@ -451,7 +514,7 @@ index 9f33c02..c311d3a 100644
|
||||
p += msgLen;
|
||||
}
|
||||
|
||||
@@ -1913,7 +1925,7 @@ class SFTP extends EventEmitter {
|
||||
@@ -1913,7 +1976,7 @@ class SFTP extends EventEmitter {
|
||||
const dataLen = (
|
||||
isBuffer
|
||||
? data.length
|
||||
@@ -460,7 +523,7 @@ index 9f33c02..c311d3a 100644
|
||||
);
|
||||
let p = 9;
|
||||
const buf = Buffer.allocUnsafe(4 + 1 + 4 + 4 + dataLen);
|
||||
@@ -1927,7 +1939,7 @@ class SFTP extends EventEmitter {
|
||||
@@ -1927,7 +1990,7 @@ class SFTP extends EventEmitter {
|
||||
if (isBuffer)
|
||||
buf.set(data, p += 4);
|
||||
else if (isUTF8)
|
||||
@@ -469,7 +532,7 @@ index 9f33c02..c311d3a 100644
|
||||
else
|
||||
buf.write(data, p += 4, dataLen, encoding);
|
||||
}
|
||||
@@ -1959,13 +1971,13 @@ class SFTP extends EventEmitter {
|
||||
@@ -1959,13 +2022,13 @@ class SFTP extends EventEmitter {
|
||||
? ''
|
||||
: name.filename
|
||||
);
|
||||
@@ -485,7 +548,7 @@ index 9f33c02..c311d3a 100644
|
||||
|
||||
if (typeof name.attrs === 'object' && name.attrs !== null) {
|
||||
nameAttrs = attrsToBytes(name.attrs);
|
||||
@@ -2011,11 +2023,11 @@ class SFTP extends EventEmitter {
|
||||
@@ -2011,11 +2074,11 @@ class SFTP extends EventEmitter {
|
||||
? ''
|
||||
: name.filename
|
||||
);
|
||||
@@ -499,7 +562,7 @@ index 9f33c02..c311d3a 100644
|
||||
p += len;
|
||||
}
|
||||
}
|
||||
@@ -2026,11 +2038,11 @@ class SFTP extends EventEmitter {
|
||||
@@ -2026,11 +2089,11 @@ class SFTP extends EventEmitter {
|
||||
? ''
|
||||
: name.longname
|
||||
);
|
||||
@@ -513,7 +576,7 @@ index 9f33c02..c311d3a 100644
|
||||
p += len;
|
||||
}
|
||||
}
|
||||
@@ -2749,7 +2761,7 @@ function requestLimits(sftp, cb) {
|
||||
@@ -2749,7 +2812,7 @@ function requestLimits(sftp, cb) {
|
||||
writeUInt32BE(buf, reqid, 5);
|
||||
|
||||
writeUInt32BE(buf, 18, p);
|
||||
@@ -522,7 +585,7 @@ index 9f33c02..c311d3a 100644
|
||||
|
||||
sftp._requests[reqid] = { extended: 'limits@openssh.com', cb };
|
||||
|
||||
@@ -2953,18 +2965,28 @@ const CLIENT_HANDLERS = {
|
||||
@@ -2953,18 +3016,28 @@ const CLIENT_HANDLERS = {
|
||||
// spec not specifying an encoding because the specs for newer
|
||||
// versions of the protocol all explicitly specify UTF-8 for
|
||||
// filenames
|
||||
|
||||
Reference in New Issue
Block a user