2025-09-12 14:42:00 -05:00
import express from "express" ;
import cors from "cors" ;
2025-10-01 15:40:10 -05:00
import cookieParser from "cookie-parser" ;
2025-11-05 10:36:16 -06:00
import axios from "axios" ;
2025-09-12 14:42:00 -05:00
import { Client as SSHClient } from "ssh2" ;
2025-10-01 15:40:10 -05:00
import { getDb } from "../database/db/index.js" ;
2025-11-05 10:36:16 -06:00
import { sshCredentials , sshData } from "../database/db/schema.js" ;
2025-09-12 14:42:00 -05:00
import { eq , and } from "drizzle-orm" ;
2025-11-17 09:46:05 -06:00
import { fileLogger , sshLogger } from "../utils/logger.js" ;
2025-10-01 15:40:10 -05:00
import { SimpleDBOps } from "../utils/simple-db-ops.js" ;
import { AuthManager } from "../utils/auth-manager.js" ;
2025-11-05 10:36:16 -06:00
import type { AuthenticatedRequest } from "../../types/index.js" ;
2025-12-31 22:20:12 -06:00
import { createSocks5Connection } from "../utils/socks5-helper.js" ;
2025-10-01 15:40:10 -05:00
function isExecutableFile ( permissions : string , fileName : string ) : boolean {
const hasExecutePermission =
permissions [ 3 ] === "x" || permissions [ 6 ] === "x" || permissions [ 9 ] === "x" ;
const scriptExtensions = [
".sh" ,
".py" ,
".pl" ,
".rb" ,
".js" ,
".php" ,
".bash" ,
".zsh" ,
".fish" ,
];
const hasScriptExtension = scriptExtensions . some (( ext ) =>
fileName . toLowerCase (). endsWith ( ext ),
);
const executableExtensions = [ ".bin" , ".exe" , ".out" ];
const hasExecutableExtension = executableExtensions . some (( ext ) =>
fileName . toLowerCase (). endsWith ( ext ),
);
const hasNoExtension = ! fileName . includes ( "." ) && hasExecutePermission ;
return (
hasExecutePermission &&
( hasScriptExtension || hasExecutableExtension || hasNoExtension )
);
}
2025-08-07 02:20:27 -05:00
const app = express ();
2025-09-12 14:42:00 -05:00
app . use (
cors ({
2025-10-01 15:40:10 -05:00
origin : ( origin , callback ) => {
if ( ! origin ) return callback ( null , true );
const allowedOrigins = [
"http://localhost:5173" ,
"http://localhost:3000" ,
"http://127.0.0.1:5173" ,
"http://127.0.0.1:3000" ,
];
if ( origin . startsWith ( "https://" )) {
return callback ( null , true );
}
if ( origin . startsWith ( "http://" )) {
return callback ( null , true );
}
if ( allowedOrigins . includes ( origin )) {
return callback ( null , true );
}
callback ( new Error ( "Not allowed by CORS" ));
},
credentials : true ,
2025-09-12 14:42:00 -05:00
methods : [ "GET" , "POST" , "PUT" , "DELETE" , "OPTIONS" ],
allowedHeaders : [
"Content-Type" ,
"Authorization" ,
"User-Agent" ,
"X-Electron-App" ,
],
}),
);
2025-10-01 15:40:10 -05:00
app . use ( cookieParser ());
app . use ( express . json ({ limit : "1gb" }));
app . use ( express . urlencoded ({ limit : "1gb" , extended : true }));
app . use ( express . raw ({ limit : "5gb" , type : "application/octet-stream" }));
const authManager = AuthManager . getInstance ();
app . use ( authManager . createAuthMiddleware ());
2025-08-07 02:20:27 -05:00
2025-11-17 09:46:05 -06:00
async function resolveJumpHost (
hostId : number ,
userId : string ,
) : Promise < any | null > {
try {
const hosts = await SimpleDBOps . select (
getDb ()
. select ()
. from ( sshData )
. where ( and ( eq ( sshData . id , hostId ), eq ( sshData . userId , userId ))),
"ssh_data" ,
userId ,
);
if ( hosts . length === 0 ) {
return null ;
}
const host = hosts [ 0 ];
if ( host . credentialId ) {
const credentials = await SimpleDBOps . select (
getDb ()
. select ()
. from ( sshCredentials )
. where (
and (
eq ( sshCredentials . id , host . credentialId as number ),
eq ( sshCredentials . userId , userId ),
),
),
"ssh_credentials" ,
userId ,
);
if ( credentials . length > 0 ) {
const credential = credentials [ 0 ];
return {
... host ,
password : credential.password ,
key :
credential.private_key || credential . privateKey || credential . key ,
keyPassword : credential.key_password || credential . keyPassword ,
keyType : credential.key_type || credential . keyType ,
authType : credential.auth_type || credential . authType ,
};
}
}
return host ;
} catch ( error ) {
fileLogger . error ( "Failed to resolve jump host" , error , {
operation : "resolve_jump_host" ,
hostId ,
userId ,
});
return null ;
}
}
async function createJumpHostChain (
jumpHosts : Array < { hostId : number } > ,
userId : string ,
) : Promise < SSHClient | null > {
if ( ! jumpHosts || jumpHosts . length === 0 ) {
return null ;
}
let currentClient : SSHClient | null = null ;
const clients : SSHClient [] = [];
try {
for ( let i = 0 ; i < jumpHosts . length ; i ++ ) {
const jumpHostConfig = await resolveJumpHost ( jumpHosts [ i ]. hostId , userId );
if ( ! jumpHostConfig ) {
fileLogger . error ( `Jump host ${ i + 1 } not found` , undefined , {
operation : "jump_host_chain" ,
hostId : jumpHosts [ i ]. hostId ,
});
clients . forEach (( c ) => c . end ());
return null ;
}
const jumpClient = new SSHClient ();
clients . push ( jumpClient );
const connected = await new Promise < boolean >(( resolve ) => {
const timeout = setTimeout (() => {
resolve ( false );
}, 30000 );
jumpClient . on ( "ready" , () => {
clearTimeout ( timeout );
resolve ( true );
});
jumpClient . on ( "error" , ( err ) => {
clearTimeout ( timeout );
fileLogger . error ( `Jump host ${ i + 1 } connection failed` , err , {
operation : "jump_host_connect" ,
hostId : jumpHostConfig.id ,
ip : jumpHostConfig.ip ,
});
resolve ( false );
});
const connectConfig : any = {
host : jumpHostConfig.ip ,
port : jumpHostConfig.port || 22 ,
username : jumpHostConfig.username ,
tryKeyboard : true ,
readyTimeout : 30000 ,
};
if ( jumpHostConfig . authType === "password" && jumpHostConfig . password ) {
connectConfig . password = jumpHostConfig . password ;
} else if ( jumpHostConfig . authType === "key" && jumpHostConfig . key ) {
const cleanKey = jumpHostConfig . key
. trim ()
. replace ( /\r\n/g , "\n" )
. replace ( /\r/g , "\n" );
connectConfig . privateKey = Buffer . from ( cleanKey , "utf8" );
if ( jumpHostConfig . keyPassword ) {
connectConfig . passphrase = jumpHostConfig . keyPassword ;
}
}
if ( currentClient ) {
currentClient . forwardOut (
"127.0.0.1" ,
0 ,
jumpHostConfig . ip ,
jumpHostConfig . port || 22 ,
( err , stream ) => {
if ( err ) {
clearTimeout ( timeout );
resolve ( false );
return ;
}
connectConfig . sock = stream ;
jumpClient . connect ( connectConfig );
},
);
} else {
jumpClient . connect ( connectConfig );
}
});
if ( ! connected ) {
clients . forEach (( c ) => c . end ());
return null ;
}
currentClient = jumpClient ;
}
return currentClient ;
} catch ( error ) {
fileLogger . error ( "Failed to create jump host chain" , error , {
operation : "jump_host_chain" ,
});
clients . forEach (( c ) => c . end ());
return null ;
}
}
2025-08-07 02:20:27 -05:00
interface SSHSession {
2025-09-12 14:42:00 -05:00
client : SSHClient ;
isConnected : boolean ;
lastActive : number ;
timeout? : NodeJS.Timeout ;
2025-11-17 09:46:05 -06:00
activeOperations : number ;
2025-08-07 02:20:27 -05:00
}
2025-11-05 10:36:16 -06:00
interface PendingTOTPSession {
client : SSHClient ;
finish : ( responses : string []) => void ;
config : import ( "ssh2" ). ConnectConfig ;
createdAt : number ;
sessionId : string ;
hostId? : number ;
ip? : string ;
port? : number ;
username? : string ;
userId? : string ;
prompts? : Array < { prompt : string ; echo : boolean } > ;
totpPromptIndex? : number ;
resolvedPassword? : string ;
2025-12-31 22:20:12 -06:00
totpAttempts : number ;
2025-11-05 10:36:16 -06:00
}
2025-08-07 02:20:27 -05:00
const sshSessions : Record < string , SSHSession > = {};
2025-11-05 10:36:16 -06:00
const pendingTOTPSessions : Record < string , PendingTOTPSession > = {};
2025-08-07 02:20:27 -05:00
function cleanupSession ( sessionId : string ) {
2025-09-12 14:42:00 -05:00
const session = sshSessions [ sessionId ];
if ( session ) {
2025-11-17 09:46:05 -06:00
if ( session . activeOperations > 0 ) {
fileLogger . warn (
`Deferring session cleanup for ${ sessionId } - ${ session . activeOperations } active operations` ,
{
operation : "cleanup_deferred" ,
sessionId ,
activeOperations : session.activeOperations ,
},
);
scheduleSessionCleanup ( sessionId );
return ;
}
2025-09-12 14:42:00 -05:00
try {
session . client . end ();
2025-11-17 09:46:05 -06:00
} catch ( error ) {}
2025-09-12 14:42:00 -05:00
clearTimeout ( session . timeout );
delete sshSessions [ sessionId ];
}
2025-08-07 02:20:27 -05:00
}
function scheduleSessionCleanup ( sessionId : string ) {
2025-09-12 14:42:00 -05:00
const session = sshSessions [ sessionId ];
if ( session ) {
if ( session . timeout ) clearTimeout ( session . timeout );
2025-10-01 15:40:10 -05:00
session . timeout = setTimeout (
() => {
cleanupSession ( sessionId );
},
30 * 60 * 1000 ,
);
2025-09-12 14:42:00 -05:00
}
2025-08-07 02:20:27 -05:00
}
2025-10-01 15:40:10 -05:00
function getMimeType ( fileName : string ) : string {
const ext = fileName . split ( "." ). pop () ? . toLowerCase ();
const mimeTypes : Record < string , string > = {
txt : "text/plain" ,
json : "application/json" ,
js : "text/javascript" ,
html : "text/html" ,
css : "text/css" ,
png : "image/png" ,
jpg : "image/jpeg" ,
jpeg : "image/jpeg" ,
gif : "image/gif" ,
pdf : "application/pdf" ,
zip : "application/zip" ,
tar : "application/x-tar" ,
gz : "application/gzip" ,
};
return mimeTypes [ ext || "" ] || "application/octet-stream" ;
}
2025-09-12 14:42:00 -05:00
app . post ( "/ssh/file_manager/ssh/connect" , async ( req , res ) => {
const {
sessionId ,
hostId ,
ip ,
port ,
username ,
password ,
sshKey ,
keyPassword ,
authType ,
credentialId ,
2025-11-05 10:36:16 -06:00
userProvidedPassword ,
forceKeyboardInteractive ,
2025-11-17 09:46:05 -06:00
jumpHosts ,
2025-12-31 22:20:12 -06:00
useSocks5 ,
socks5Host ,
socks5Port ,
socks5Username ,
socks5Password ,
socks5ProxyChain ,
2025-09-12 14:42:00 -05:00
} = req . body ;
2025-08-07 02:20:27 -05:00
2025-11-05 10:36:16 -06:00
const userId = ( req as AuthenticatedRequest ). userId ;
2025-10-01 15:40:10 -05:00
if ( ! userId ) {
fileLogger . error ( "SSH connection rejected: no authenticated user" , {
operation : "file_connect_auth" ,
sessionId ,
});
return res . status ( 401 ). json ({ error : "Authentication required" });
}
2025-09-12 14:42:00 -05:00
if ( ! sessionId || ! ip || ! username || ! port ) {
fileLogger . warn ( "Missing SSH connection parameters for file manager" , {
operation : "file_connect" ,
sessionId ,
hasIp : !! ip ,
hasUsername : !! username ,
hasPort : !! port ,
2025-08-07 02:20:27 -05:00
});
2025-09-12 14:42:00 -05:00
return res . status ( 400 ). json ({ error : "Missing SSH connection parameters" });
}
2025-08-07 02:20:27 -05:00
2025-09-12 14:42:00 -05:00
if ( sshSessions [ sessionId ] ? . isConnected ) {
2025-08-07 02:20:27 -05:00
cleanupSession ( sessionId );
2025-09-12 14:42:00 -05:00
}
2025-12-31 22:20:12 -06:00
// Clean up any stale pending TOTP sessions
if ( pendingTOTPSessions [ sessionId ]) {
try {
pendingTOTPSessions [ sessionId ]. client . end ();
} catch {}
delete pendingTOTPSessions [ sessionId ];
}
2025-09-12 14:42:00 -05:00
const client = new SSHClient ();
2025-08-07 02:20:27 -05:00
2025-09-12 14:42:00 -05:00
let resolvedCredentials = { password , sshKey , keyPassword , authType };
if ( credentialId && hostId && userId ) {
try {
2025-10-01 15:40:10 -05:00
const credentials = await SimpleDBOps . select (
getDb ()
. select ()
. from ( sshCredentials )
. where (
and (
eq ( sshCredentials . id , credentialId ),
eq ( sshCredentials . userId , userId ),
),
2025-09-12 14:42:00 -05:00
),
2025-10-01 15:40:10 -05:00
"ssh_credentials" ,
userId ,
);
2025-08-07 02:20:27 -05:00
2025-09-12 14:42:00 -05:00
if ( credentials . length > 0 ) {
const credential = credentials [ 0 ];
resolvedCredentials = {
password : credential.password ,
2025-10-03 00:02:10 -05:00
sshKey :
credential.private_key || credential . privateKey || credential . key ,
keyPassword : credential.key_password || credential . keyPassword ,
authType : credential.auth_type || credential . authType ,
2025-09-12 14:42:00 -05:00
};
} else {
2025-10-01 15:40:10 -05:00
fileLogger . warn ( `No credentials found for host ${ hostId } ` , {
operation : "ssh_credentials" ,
2025-09-12 14:42:00 -05:00
hostId ,
credentialId ,
userId ,
});
}
} catch ( error ) {
2025-10-01 15:40:10 -05:00
fileLogger . warn ( `Failed to resolve credentials for host ${ hostId } ` , {
operation : "ssh_credentials" ,
hostId ,
credentialId ,
error : error instanceof Error ? error . message : "Unknown error" ,
});
2025-08-07 02:20:27 -05:00
}
2025-09-12 14:42:00 -05:00
} else if ( credentialId && hostId ) {
fileLogger . warn (
"Missing userId for credential resolution in file manager" ,
{
2025-10-01 15:40:10 -05:00
operation : "ssh_credentials" ,
2025-09-12 14:42:00 -05:00
hostId ,
credentialId ,
hasUserId : !! userId ,
},
);
}
2025-08-07 02:20:27 -05:00
2025-11-05 10:36:16 -06:00
const config : Record < string , unknown > = {
2025-09-12 14:42:00 -05:00
host : ip ,
2025-11-05 10:36:16 -06:00
port ,
2025-09-12 14:42:00 -05:00
username ,
2025-11-05 10:36:16 -06:00
tryKeyboard : true ,
2025-09-12 14:42:00 -05:00
keepaliveInterval : 30000 ,
2025-10-01 15:40:10 -05:00
keepaliveCountMax : 3 ,
2025-11-05 10:36:16 -06:00
readyTimeout : 60000 ,
tcpKeepAlive : true ,
tcpKeepAliveInitialDelay : 30000 ,
env : {
TERM : "xterm-256color" ,
LANG : "en_US.UTF-8" ,
LC_ALL : "en_US.UTF-8" ,
LC_CTYPE : "en_US.UTF-8" ,
LC_MESSAGES : "en_US.UTF-8" ,
LC_MONETARY : "en_US.UTF-8" ,
LC_NUMERIC : "en_US.UTF-8" ,
LC_TIME : "en_US.UTF-8" ,
LC_COLLATE : "en_US.UTF-8" ,
COLORTERM : "truecolor" ,
},
2025-09-12 14:42:00 -05:00
algorithms : {
kex : [
2025-11-05 10:36:16 -06:00
"curve25519-sha256" ,
"curve25519-sha256@libssh.org" ,
"ecdh-sha2-nistp521" ,
"ecdh-sha2-nistp384" ,
"ecdh-sha2-nistp256" ,
"diffie-hellman-group-exchange-sha256" ,
2025-09-12 14:42:00 -05:00
"diffie-hellman-group14-sha256" ,
"diffie-hellman-group14-sha1" ,
"diffie-hellman-group-exchange-sha1" ,
2025-11-05 10:36:16 -06:00
"diffie-hellman-group1-sha1" ,
],
serverHostKey : [
"ssh-ed25519" ,
"ecdsa-sha2-nistp521" ,
"ecdsa-sha2-nistp384" ,
"ecdsa-sha2-nistp256" ,
"rsa-sha2-512" ,
"rsa-sha2-256" ,
"ssh-rsa" ,
"ssh-dss" ,
2025-09-12 14:42:00 -05:00
],
cipher : [
2025-11-05 10:36:16 -06:00
"chacha20-poly1305@openssh.com" ,
2025-09-12 14:42:00 -05:00
"aes256-gcm@openssh.com" ,
2025-11-05 10:36:16 -06:00
"aes128-gcm@openssh.com" ,
"aes256-ctr" ,
"aes192-ctr" ,
"aes128-ctr" ,
2025-09-12 14:42:00 -05:00
"aes256-cbc" ,
2025-11-05 10:36:16 -06:00
"aes192-cbc" ,
"aes128-cbc" ,
2025-09-12 14:42:00 -05:00
"3des-cbc" ,
],
2025-10-01 15:40:10 -05:00
hmac : [
"hmac-sha2-512-etm@openssh.com" ,
2025-11-05 10:36:16 -06:00
"hmac-sha2-256-etm@openssh.com" ,
2025-10-01 15:40:10 -05:00
"hmac-sha2-512" ,
2025-11-05 10:36:16 -06:00
"hmac-sha2-256" ,
2025-10-01 15:40:10 -05:00
"hmac-sha1" ,
"hmac-md5" ,
],
2025-09-12 14:42:00 -05:00
compress : [ "none" , "zlib@openssh.com" , "zlib" ],
},
};
2025-10-01 15:40:10 -05:00
if (
resolvedCredentials . authType === "key" &&
resolvedCredentials . sshKey &&
resolvedCredentials . sshKey . trim ()
) {
2025-09-12 14:42:00 -05:00
try {
if (
! resolvedCredentials . sshKey . includes ( "-----BEGIN" ) ||
! resolvedCredentials . sshKey . includes ( "-----END" )
) {
throw new Error ( "Invalid private key format" );
}
const cleanKey = resolvedCredentials . sshKey
. trim ()
. replace ( /\r\n/g , "\n" )
. replace ( /\r/g , "\n" );
config . privateKey = Buffer . from ( cleanKey , "utf8" );
if ( resolvedCredentials . keyPassword )
config . passphrase = resolvedCredentials . keyPassword ;
} catch ( keyError ) {
fileLogger . error ( "SSH key format error for file manager" , {
operation : "file_connect" ,
sessionId ,
hostId ,
error : keyError.message ,
});
return res . status ( 400 ). json ({ error : "Invalid SSH key format" });
2025-08-07 02:20:27 -05:00
}
2025-11-05 10:36:16 -06:00
} else if ( resolvedCredentials . authType === "password" ) {
if ( ! resolvedCredentials . password || ! resolvedCredentials . password . trim ()) {
return res
. status ( 400 )
. json ({ error : "Password required for password authentication" });
}
2025-12-31 22:20:12 -06:00
config . password = resolvedCredentials . password ;
2025-11-05 10:36:16 -06:00
} else if ( resolvedCredentials . authType === "none" ) {
2025-09-12 14:42:00 -05:00
} else {
2025-10-01 15:40:10 -05:00
fileLogger . warn (
"No valid authentication method provided for file manager" ,
{
operation : "file_connect" ,
sessionId ,
hostId ,
authType : resolvedCredentials.authType ,
hasPassword : !! resolvedCredentials . password ,
hasKey : !! resolvedCredentials . sshKey ,
},
);
2025-09-12 14:42:00 -05:00
return res
. status ( 400 )
. json ({ error : "Either password or SSH key must be provided" });
}
2025-08-07 02:20:27 -05:00
2025-09-12 14:42:00 -05:00
let responseSent = false ;
2025-08-07 02:20:27 -05:00
2025-09-12 14:42:00 -05:00
client . on ( "ready" , () => {
if ( responseSent ) return ;
responseSent = true ;
sshSessions [ sessionId ] = {
client ,
isConnected : true ,
lastActive : Date.now (),
2025-11-17 09:46:05 -06:00
activeOperations : 0 ,
2025-08-17 22:57:25 -05:00
};
2025-10-01 15:40:10 -05:00
scheduleSessionCleanup ( sessionId );
2025-09-12 14:42:00 -05:00
res . json ({ status : "success" , message : "SSH connection established" });
2025-11-05 10:36:16 -06:00
if ( hostId && userId ) {
( async () => {
try {
const hosts = await SimpleDBOps . select (
getDb ()
. select ()
. from ( sshData )
. where ( and ( eq ( sshData . id , hostId ), eq ( sshData . userId , userId ))),
"ssh_data" ,
userId ,
);
const hostName =
hosts . length > 0 && hosts [ 0 ]. name
? hosts [ 0 ]. name
: ` ${ username } @ ${ ip } : ${ port } ` ;
const authManager = AuthManager . getInstance ();
await axios . post (
"http://localhost:30006/activity/log" ,
{
type : "file_manager" ,
hostId ,
hostName ,
},
{
headers : {
Authorization : `Bearer ${ await authManager . generateJWTToken ( userId ) } ` ,
},
},
);
} catch ( error ) {
fileLogger . warn ( "Failed to log file manager activity" , {
operation : "activity_log_error" ,
userId ,
hostId ,
error : error instanceof Error ? error . message : "Unknown error" ,
});
}
})();
}
2025-09-12 14:42:00 -05:00
});
2025-08-07 02:20:27 -05:00
2025-09-12 14:42:00 -05:00
client . on ( "error" , ( err ) => {
if ( responseSent ) return ;
responseSent = true ;
fileLogger . error ( "SSH connection failed for file manager" , {
operation : "file_connect" ,
sessionId ,
hostId ,
ip ,
port ,
username ,
error : err.message ,
});
2025-11-05 10:36:16 -06:00
if (
resolvedCredentials . authType === "none" &&
( err . message . includes ( "authentication" ) ||
err . message . includes ( "All configured authentication methods failed" ))
) {
res . json ({
status : "auth_required" ,
reason : "no_keyboard" ,
});
} else {
res . status ( 500 ). json ({ status : "error" , message : err.message });
}
2025-09-12 14:42:00 -05:00
});
client . on ( "close" , () => {
if ( sshSessions [ sessionId ]) sshSessions [ sessionId ]. isConnected = false ;
cleanupSession ( sessionId );
});
2025-11-05 10:36:16 -06:00
let keyboardInteractiveResponded = false ;
client . on (
"keyboard-interactive" ,
(
name : string ,
instructions : string ,
instructionsLang : string ,
prompts : Array < { prompt : string ; echo : boolean } > ,
finish : ( responses : string []) => void ,
) => {
const promptTexts = prompts . map (( p ) => p . prompt );
const totpPromptIndex = prompts . findIndex (( p ) =>
/verification code|verification_code|token|otp|2fa|authenticator|google.*auth/i . test (
p . prompt ,
),
);
if ( totpPromptIndex !== - 1 ) {
if ( responseSent ) {
const responses = prompts . map (( p ) => {
if ( /password/i . test ( p . prompt ) && resolvedCredentials . password ) {
return resolvedCredentials . password ;
}
return "" ;
});
finish ( responses );
return ;
}
responseSent = true ;
if ( pendingTOTPSessions [ sessionId ]) {
const responses = prompts . map (( p ) => {
if ( /password/i . test ( p . prompt ) && resolvedCredentials . password ) {
return resolvedCredentials . password ;
}
return "" ;
});
finish ( responses );
return ;
}
keyboardInteractiveResponded = true ;
pendingTOTPSessions [ sessionId ] = {
client ,
finish ,
config ,
createdAt : Date.now (),
sessionId ,
hostId ,
ip ,
port ,
username ,
userId ,
prompts ,
totpPromptIndex ,
resolvedPassword : resolvedCredentials.password ,
2025-12-31 22:20:12 -06:00
totpAttempts : 0 ,
2025-11-05 10:36:16 -06:00
};
res . json ({
requires_totp : true ,
sessionId ,
prompt : prompts [ totpPromptIndex ]. prompt ,
});
} else {
const hasStoredPassword =
resolvedCredentials . password &&
resolvedCredentials . authType !== "none" ;
const passwordPromptIndex = prompts . findIndex (( p ) =>
/password/i . test ( p . prompt ),
);
if (
resolvedCredentials . authType === "none" &&
passwordPromptIndex !== - 1
) {
if ( responseSent ) return ;
responseSent = true ;
client . end ();
res . json ({
status : "auth_required" ,
reason : "no_keyboard" ,
});
return ;
}
if ( ! hasStoredPassword && passwordPromptIndex !== - 1 ) {
if ( responseSent ) {
const responses = prompts . map (( p ) => {
if ( /password/i . test ( p . prompt ) && resolvedCredentials . password ) {
return resolvedCredentials . password ;
}
return "" ;
});
finish ( responses );
return ;
}
responseSent = true ;
if ( pendingTOTPSessions [ sessionId ]) {
const responses = prompts . map (( p ) => {
if ( /password/i . test ( p . prompt ) && resolvedCredentials . password ) {
return resolvedCredentials . password ;
}
return "" ;
});
finish ( responses );
return ;
}
keyboardInteractiveResponded = true ;
pendingTOTPSessions [ sessionId ] = {
client ,
finish ,
config ,
createdAt : Date.now (),
sessionId ,
hostId ,
ip ,
port ,
username ,
userId ,
prompts ,
totpPromptIndex : passwordPromptIndex ,
resolvedPassword : resolvedCredentials.password ,
2025-12-31 22:20:12 -06:00
totpAttempts : 0 ,
2025-11-05 10:36:16 -06:00
};
res . json ({
requires_totp : true ,
sessionId ,
prompt : prompts [ passwordPromptIndex ]. prompt ,
isPassword : true ,
});
return ;
}
const responses = prompts . map (( p ) => {
if ( /password/i . test ( p . prompt ) && resolvedCredentials . password ) {
return resolvedCredentials . password ;
}
return "" ;
});
finish ( responses );
}
},
);
2025-12-31 22:20:12 -06:00
if (
useSocks5 &&
( socks5Host || ( socks5ProxyChain && ( socks5ProxyChain as any ). length > 0 ))
) {
try {
const socks5Socket = await createSocks5Connection ( ip , port , {
useSocks5 ,
socks5Host ,
socks5Port ,
socks5Username ,
socks5Password ,
socks5ProxyChain : socks5ProxyChain as any ,
});
if ( socks5Socket ) {
config . sock = socks5Socket ;
client . connect ( config );
return ;
} else {
fileLogger . error ( "SOCKS5 socket is null for SFTP" , undefined , {
operation : "sftp_socks5_socket_null" ,
sessionId ,
});
}
} catch ( socks5Error ) {
fileLogger . error ( "SOCKS5 connection failed" , socks5Error , {
operation : "socks5_connect" ,
sessionId ,
hostId ,
proxyHost : socks5Host ,
proxyPort : socks5Port || 1080 ,
});
return res . status ( 500 ). json ({
error :
"SOCKS5 proxy connection failed: " +
( socks5Error instanceof Error
? socks5Error . message
: "Unknown error" ),
});
}
} else if ( jumpHosts && jumpHosts . length > 0 && userId ) {
2025-11-17 09:46:05 -06:00
try {
const jumpClient = await createJumpHostChain ( jumpHosts , userId );
if ( ! jumpClient ) {
fileLogger . error ( "Failed to establish jump host chain" , {
operation : "file_jump_chain" ,
sessionId ,
hostId ,
});
return res
. status ( 500 )
. json ({ error : "Failed to connect through jump hosts" });
}
jumpClient . forwardOut ( "127.0.0.1" , 0 , ip , port , ( err , stream ) => {
if ( err ) {
fileLogger . error ( "Failed to forward through jump host" , err , {
operation : "file_jump_forward" ,
sessionId ,
hostId ,
ip ,
port ,
});
jumpClient . end ();
return res . status ( 500 ). json ({
error : "Failed to forward through jump host: " + err . message ,
});
}
config . sock = stream ;
client . connect ( config );
});
} catch ( error ) {
fileLogger . error ( "Jump host error" , error , {
operation : "file_jump_host" ,
sessionId ,
hostId ,
});
return res
. status ( 500 )
. json ({ error : "Failed to connect through jump hosts" });
}
} else {
client . connect ( config );
}
2025-09-12 14:42:00 -05:00
});
2025-11-05 10:36:16 -06:00
app . post ( "/ssh/file_manager/ssh/connect-totp" , async ( req , res ) => {
const { sessionId , totpCode } = req . body ;
const userId = ( req as AuthenticatedRequest ). userId ;
if ( ! userId ) {
fileLogger . error ( "TOTP verification rejected: no authenticated user" , {
operation : "file_totp_auth" ,
sessionId ,
});
return res . status ( 401 ). json ({ error : "Authentication required" });
}
if ( ! sessionId || ! totpCode ) {
return res . status ( 400 ). json ({ error : "Session ID and TOTP code required" });
}
const session = pendingTOTPSessions [ sessionId ];
if ( ! session ) {
fileLogger . warn ( "TOTP session not found or expired" , {
operation : "file_totp_verify" ,
sessionId ,
userId ,
availableSessions : Object.keys ( pendingTOTPSessions ),
});
return res
. status ( 404 )
. json ({ error : "TOTP session expired. Please reconnect." });
}
if ( Date . now () - session . createdAt > 180000 ) {
delete pendingTOTPSessions [ sessionId ];
try {
session . client . end ();
2025-12-31 22:20:12 -06:00
} catch ( error ) {}
2025-11-05 10:36:16 -06:00
fileLogger . warn ( "TOTP session timeout before code submission" , {
operation : "file_totp_verify" ,
sessionId ,
userId ,
age : Date.now () - session . createdAt ,
});
return res
. status ( 408 )
. json ({ error : "TOTP session timeout. Please reconnect." });
}
const responses = ( session . prompts || []). map (( p , index ) => {
if ( index === session . totpPromptIndex ) {
return totpCode ;
}
if ( /password/i . test ( p . prompt ) && session . resolvedPassword ) {
return session . resolvedPassword ;
}
return "" ;
});
let responseSent = false ;
let responseTimeout : NodeJS.Timeout ;
session . client . once ( "ready" , () => {
if ( responseSent ) return ;
responseSent = true ;
clearTimeout ( responseTimeout );
delete pendingTOTPSessions [ sessionId ];
setTimeout (() => {
sshSessions [ sessionId ] = {
client : session.client ,
isConnected : true ,
lastActive : Date.now (),
2025-11-17 09:46:05 -06:00
activeOperations : 0 ,
2025-11-05 10:36:16 -06:00
};
scheduleSessionCleanup ( sessionId );
res . json ({
status : "success" ,
message : "TOTP verified, SSH connection established" ,
});
if ( session . hostId && session . userId ) {
( async () => {
try {
const hosts = await SimpleDBOps . select (
getDb ()
. select ()
. from ( sshData )
. where (
and (
eq ( sshData . id , session . hostId ! ),
eq ( sshData . userId , session . userId ! ),
),
),
"ssh_data" ,
session . userId ! ,
);
const hostName =
hosts . length > 0 && hosts [ 0 ]. name
? hosts [ 0 ]. name
: ` ${ session . username } @ ${ session . ip } : ${ session . port } ` ;
const authManager = AuthManager . getInstance ();
await axios . post (
"http://localhost:30006/activity/log" ,
{
type : "file_manager" ,
hostId : session.hostId ,
hostName ,
},
{
headers : {
Authorization : `Bearer ${ await authManager . generateJWTToken ( session . userId ! ) } ` ,
},
},
);
} catch ( error ) {
fileLogger . warn ( "Failed to log file manager activity (TOTP)" , {
operation : "activity_log_error" ,
userId : session.userId ,
hostId : session.hostId ,
error : error instanceof Error ? error . message : "Unknown error" ,
});
}
})();
}
}, 200 );
});
session . client . once ( "error" , ( err ) => {
if ( responseSent ) return ;
responseSent = true ;
clearTimeout ( responseTimeout );
delete pendingTOTPSessions [ sessionId ];
fileLogger . error ( "TOTP verification failed" , {
operation : "file_totp_verify" ,
sessionId ,
userId ,
error : err.message ,
});
res . status ( 401 ). json ({ status : "error" , message : "Invalid TOTP code" });
});
responseTimeout = setTimeout (() => {
if ( ! responseSent ) {
responseSent = true ;
delete pendingTOTPSessions [ sessionId ];
fileLogger . warn ( "TOTP verification timeout" , {
operation : "file_totp_verify" ,
sessionId ,
userId ,
});
res . status ( 408 ). json ({ error : "TOTP verification timeout" });
}
}, 60000 );
session . finish ( responses );
});
2025-09-12 14:42:00 -05:00
app . post ( "/ssh/file_manager/ssh/disconnect" , ( req , res ) => {
const { sessionId } = req . body ;
cleanupSession ( sessionId );
res . json ({ status : "success" , message : "SSH connection disconnected" });
});
app . get ( "/ssh/file_manager/ssh/status" , ( req , res ) => {
const sessionId = req . query . sessionId as string ;
const isConnected = !! sshSessions [ sessionId ] ? . isConnected ;
res . json ({ status : "success" , connected : isConnected });
});
2025-10-01 15:40:10 -05:00
app . post ( "/ssh/file_manager/ssh/keepalive" , ( req , res ) => {
const { sessionId } = req . body ;
if ( ! sessionId ) {
return res . status ( 400 ). json ({ error : "Session ID is required" });
}
const session = sshSessions [ sessionId ];
if ( ! session || ! session . isConnected ) {
return res . status ( 400 ). json ({
error : "SSH session not found or not connected" ,
connected : false ,
});
}
session . lastActive = Date . now ();
scheduleSessionCleanup ( sessionId );
res . json ({
status : "success" ,
connected : true ,
message : "Session keepalive successful" ,
lastActive : session.lastActive ,
});
});
2025-09-12 14:42:00 -05:00
app . get ( "/ssh/file_manager/ssh/listFiles" , ( req , res ) => {
const sessionId = req . query . sessionId as string ;
const sshConn = sshSessions [ sessionId ];
const sshPath = decodeURIComponent (( req . query . path as string ) || "/" );
if ( ! sessionId ) {
return res . status ( 400 ). json ({ error : "Session ID is required" });
}
if ( ! sshConn ? . isConnected ) {
return res . status ( 400 ). json ({ error : "SSH connection not established" });
}
sshConn . lastActive = Date . now ();
2025-11-17 09:46:05 -06:00
sshConn . activeOperations ++ ;
2025-09-12 14:42:00 -05:00
const escapedPath = sshPath . replace ( /'/g , "'\"'\"'" );
2025-11-17 09:46:05 -06:00
sshConn . client . exec ( `command ls -la ' ${ escapedPath } '` , ( err , stream ) => {
2025-09-12 14:42:00 -05:00
if ( err ) {
2025-11-17 09:46:05 -06:00
sshConn . activeOperations -- ;
2025-09-12 14:42:00 -05:00
fileLogger . error ( "SSH listFiles error:" , err );
return res . status ( 500 ). json ({ error : err.message });
}
let data = "" ;
let errorData = "" ;
stream . on ( "data" , ( chunk : Buffer ) => {
data += chunk . toString ();
});
stream . stderr . on ( "data" , ( chunk : Buffer ) => {
errorData += chunk . toString ();
});
stream . on ( "close" , ( code ) => {
2025-11-17 09:46:05 -06:00
sshConn . activeOperations -- ;
2025-09-12 14:42:00 -05:00
if ( code !== 0 ) {
fileLogger . error (
`SSH listFiles command failed with code ${ code } : ${ errorData . replace ( /\n/g , " " ). trim () } ` ,
);
return res . status ( 500 ). json ({ error : `Command failed: ${ errorData } ` });
}
const lines = data . split ( "\n" ). filter (( line ) => line . trim ());
const files = [];
for ( let i = 1 ; i < lines . length ; i ++ ) {
const line = lines [ i ];
const parts = line . split ( /\s+/ );
if ( parts . length >= 9 ) {
const permissions = parts [ 0 ];
2025-10-01 15:40:10 -05:00
const owner = parts [ 2 ];
const group = parts [ 3 ];
const size = parseInt ( parts [ 4 ], 10 );
let dateStr = "" ;
2025-11-05 10:36:16 -06:00
const nameStartIndex = 8 ;
2025-10-01 15:40:10 -05:00
if ( parts [ 5 ] && parts [ 6 ] && parts [ 7 ]) {
dateStr = ` ${ parts [ 5 ] } ${ parts [ 6 ] } ${ parts [ 7 ] } ` ;
}
const name = parts . slice ( nameStartIndex ). join ( " " );
2025-09-12 14:42:00 -05:00
const isDirectory = permissions . startsWith ( "d" );
const isLink = permissions . startsWith ( "l" );
if ( name === "." || name === ".." ) continue ;
2025-10-01 15:40:10 -05:00
let actualName = name ;
let linkTarget = undefined ;
if ( isLink && name . includes ( " -> " )) {
const linkParts = name . split ( " -> " );
actualName = linkParts [ 0 ];
linkTarget = linkParts [ 1 ];
}
2025-09-12 14:42:00 -05:00
files . push ({
2025-10-01 15:40:10 -05:00
name : actualName ,
2025-09-12 14:42:00 -05:00
type : isDirectory ? "directory" : isLink ? "link" : "file" ,
2025-10-01 15:40:10 -05:00
size : isDirectory ? undefined : size ,
modified : dateStr ,
permissions ,
owner ,
group ,
linkTarget ,
path : ` ${ sshPath . endsWith ( "/" ) ? sshPath : sshPath + "/" }${ actualName } ` ,
executable :
! isDirectory && ! isLink
? isExecutableFile ( permissions , actualName )
: false ,
2025-09-12 14:42:00 -05:00
});
}
}
2025-10-01 15:40:10 -05:00
res . json ({ files , path : sshPath });
});
});
});
app . get ( "/ssh/file_manager/ssh/identifySymlink" , ( req , res ) => {
const sessionId = req . query . sessionId as string ;
const sshConn = sshSessions [ sessionId ];
const linkPath = decodeURIComponent ( req . query . path as string );
if ( ! sessionId ) {
return res . status ( 400 ). json ({ error : "Session ID is required" });
}
if ( ! sshConn ? . isConnected ) {
return res . status ( 400 ). json ({ error : "SSH connection not established" });
}
if ( ! linkPath ) {
return res . status ( 400 ). json ({ error : "Link path is required" });
}
sshConn . lastActive = Date . now ();
const escapedPath = linkPath . replace ( /'/g , "'\"'\"'" );
const command = `stat -L -c "%F" ' ${ escapedPath } ' && readlink -f ' ${ escapedPath } '` ;
sshConn . client . exec ( command , ( err , stream ) => {
if ( err ) {
fileLogger . error ( "SSH identifySymlink error:" , err );
return res . status ( 500 ). json ({ error : err.message });
}
let data = "" ;
let errorData = "" ;
stream . on ( "data" , ( chunk : Buffer ) => {
data += chunk . toString ();
});
stream . stderr . on ( "data" , ( chunk : Buffer ) => {
errorData += chunk . toString ();
});
stream . on ( "close" , ( code ) => {
if ( code !== 0 ) {
fileLogger . error (
`SSH identifySymlink command failed with code ${ code } : ${ errorData . replace ( /\n/g , " " ). trim () } ` ,
);
return res . status ( 500 ). json ({ error : `Command failed: ${ errorData } ` });
}
const [ fileType , target ] = data . trim (). split ( "\n" );
res . json ({
path : linkPath ,
target : target ,
type : fileType . toLowerCase (). includes ( "directory" )
? "directory"
: "file" ,
});
});
stream . on ( "error" , ( streamErr ) => {
fileLogger . error ( "SSH identifySymlink stream error:" , streamErr );
if ( ! res . headersSent ) {
res . status ( 500 ). json ({ error : `Stream error: ${ streamErr . message } ` });
}
2025-09-12 14:42:00 -05:00
});
});
});
app . get ( "/ssh/file_manager/ssh/readFile" , ( req , res ) => {
const sessionId = req . query . sessionId as string ;
const sshConn = sshSessions [ sessionId ];
const filePath = decodeURIComponent ( req . query . path as string );
if ( ! sessionId ) {
return res . status ( 400 ). json ({ error : "Session ID is required" });
}
if ( ! sshConn ? . isConnected ) {
return res . status ( 400 ). json ({ error : "SSH connection not established" });
}
if ( ! filePath ) {
return res . status ( 400 ). json ({ error : "File path is required" });
}
sshConn . lastActive = Date . now ();
2025-10-01 15:40:10 -05:00
const MAX_READ_SIZE = 500 * 1024 * 1024 ;
2025-09-12 14:42:00 -05:00
const escapedPath = filePath . replace ( /'/g , "'\"'\"'" );
2025-10-01 15:40:10 -05:00
sshConn . client . exec (
`stat -c%s ' ${ escapedPath } ' 2>/dev/null || wc -c < ' ${ escapedPath } '` ,
( sizeErr , sizeStream ) => {
if ( sizeErr ) {
fileLogger . error ( "SSH file size check error:" , sizeErr );
return res . status ( 500 ). json ({ error : sizeErr.message });
2025-09-12 14:42:00 -05:00
}
2025-10-01 15:40:10 -05:00
let sizeData = "" ;
let sizeErrorData = "" ;
sizeStream . on ( "data" , ( chunk : Buffer ) => {
sizeData += chunk . toString ();
});
sizeStream . stderr . on ( "data" , ( chunk : Buffer ) => {
sizeErrorData += chunk . toString ();
});
sizeStream . on ( "close" , ( sizeCode ) => {
if ( sizeCode !== 0 ) {
const errorLower = sizeErrorData . toLowerCase ();
const isFileNotFound =
errorLower . includes ( "no such file or directory" ) ||
errorLower . includes ( "cannot access" ) ||
errorLower . includes ( "not found" ) ||
errorLower . includes ( "resource not found" );
fileLogger . error ( `File size check failed: ${ sizeErrorData } ` );
return res . status ( isFileNotFound ? 404 : 500 ). json ({
error : `Cannot check file size: ${ sizeErrorData } ` ,
fileNotFound : isFileNotFound ,
});
}
const fileSize = parseInt ( sizeData . trim (), 10 );
if ( isNaN ( fileSize )) {
fileLogger . error ( "Invalid file size response:" , sizeData );
return res . status ( 500 ). json ({ error : "Cannot determine file size" });
}
if ( fileSize > MAX_READ_SIZE ) {
fileLogger . warn ( "File too large for reading" , {
operation : "file_read" ,
sessionId ,
filePath ,
fileSize ,
maxSize : MAX_READ_SIZE ,
});
return res . status ( 400 ). json ({
error : `File too large to open in editor. Maximum size is ${ MAX_READ_SIZE / 1024 / 1024 } MB, file is ${ ( fileSize / 1024 / 1024 ). toFixed ( 2 ) } MB. Use download instead.` ,
fileSize ,
maxSize : MAX_READ_SIZE ,
tooLarge : true ,
});
}
sshConn . client . exec ( `cat ' ${ escapedPath } '` , ( err , stream ) => {
if ( err ) {
fileLogger . error ( "SSH readFile error:" , err );
return res . status ( 500 ). json ({ error : err.message });
}
let data = "" ;
let errorData = "" ;
stream . on ( "data" , ( chunk : Buffer ) => {
data += chunk . toString ();
});
stream . stderr . on ( "data" , ( chunk : Buffer ) => {
errorData += chunk . toString ();
});
stream . on ( "close" , ( code ) => {
if ( code !== 0 ) {
fileLogger . error (
`SSH readFile command failed with code ${ code } : ${ errorData . replace ( /\n/g , " " ). trim () } ` ,
);
const isFileNotFound =
errorData . includes ( "No such file or directory" ) ||
errorData . includes ( "cannot access" ) ||
errorData . includes ( "not found" );
return res . status ( isFileNotFound ? 404 : 500 ). json ({
error : `Command failed: ${ errorData } ` ,
fileNotFound : isFileNotFound ,
});
}
res . json ({ content : data , path : filePath });
});
});
});
},
);
2025-09-12 14:42:00 -05:00
});
app . post ( "/ssh/file_manager/ssh/writeFile" , async ( req , res ) => {
2025-11-05 10:36:16 -06:00
const { sessionId , path : filePath , content } = req . body ;
2025-09-12 14:42:00 -05:00
const sshConn = sshSessions [ sessionId ];
if ( ! sessionId ) {
return res . status ( 400 ). json ({ error : "Session ID is required" });
}
if ( ! sshConn ? . isConnected ) {
return res . status ( 400 ). json ({ error : "SSH connection not established" });
}
if ( ! filePath ) {
return res . status ( 400 ). json ({ error : "File path is required" });
}
if ( content === undefined ) {
return res . status ( 400 ). json ({ error : "File content is required" });
}
sshConn . lastActive = Date . now ();
const trySFTP = () => {
try {
sshConn . client . sftp (( err , sftp ) => {
if ( err ) {
fileLogger . warn (
`SFTP failed, trying fallback method: ${ err . message } ` ,
);
tryFallbackMethod ();
return ;
}
let fileBuffer ;
2025-08-17 22:57:25 -05:00
try {
2025-09-12 14:42:00 -05:00
if ( typeof content === "string" ) {
2025-12-31 22:20:12 -06:00
fileBuffer = Buffer . from ( content , "base64" );
2025-09-12 14:42:00 -05:00
} else if ( Buffer . isBuffer ( content )) {
fileBuffer = content ;
} else {
fileBuffer = Buffer . from ( content );
}
} catch ( bufferErr ) {
fileLogger . error ( "Buffer conversion error:" , bufferErr );
if ( ! res . headersSent ) {
return res
. status ( 500 )
. json ({ error : "Invalid file content format" });
}
return ;
}
2025-08-07 02:20:27 -05:00
2025-09-12 14:42:00 -05:00
const writeStream = sftp . createWriteStream ( filePath );
2025-08-18 00:13:21 -05:00
2025-09-12 14:42:00 -05:00
let hasError = false ;
let hasFinished = false ;
2025-08-27 22:17:28 -05:00
2025-09-12 14:42:00 -05:00
writeStream . on ( "error" , ( streamErr ) => {
if ( hasError || hasFinished ) return ;
hasError = true ;
fileLogger . warn (
`SFTP write failed, trying fallback method: ${ streamErr . message } ` ,
);
tryFallbackMethod ();
});
2025-08-07 02:20:27 -05:00
2025-09-12 14:42:00 -05:00
writeStream . on ( "finish" , () => {
if ( hasError || hasFinished ) return ;
hasFinished = true ;
if ( ! res . headersSent ) {
res . json ({
message : "File written successfully" ,
path : filePath ,
toast : { type : "success" , message : `File written: ${ filePath } ` },
2025-08-07 02:20:27 -05:00
});
2025-09-12 14:42:00 -05:00
}
});
2025-08-27 22:17:28 -05:00
2025-09-12 14:42:00 -05:00
writeStream . on ( "close" , () => {
if ( hasError || hasFinished ) return ;
hasFinished = true ;
if ( ! res . headersSent ) {
res . json ({
message : "File written successfully" ,
path : filePath ,
toast : { type : "success" , message : `File written: ${ filePath } ` },
});
}
});
try {
writeStream . write ( fileBuffer );
writeStream . end ();
} catch ( writeErr ) {
if ( hasError || hasFinished ) return ;
hasError = true ;
fileLogger . warn (
`SFTP write operation failed, trying fallback method: ${ writeErr . message } ` ,
);
tryFallbackMethod ();
}
});
} catch ( sftpErr ) {
fileLogger . warn (
`SFTP connection error, trying fallback method: ${ sftpErr . message } ` ,
);
tryFallbackMethod ();
}
};
const tryFallbackMethod = () => {
try {
2025-12-31 22:20:12 -06:00
let contentBuffer : Buffer ;
if ( typeof content === "string" ) {
try {
contentBuffer = Buffer . from ( content , "base64" );
if ( contentBuffer . toString ( "base64" ) !== content ) {
contentBuffer = Buffer . from ( content , "utf8" );
}
} catch {
contentBuffer = Buffer . from ( content , "utf8" );
}
} else if ( Buffer . isBuffer ( content )) {
contentBuffer = content ;
} else {
contentBuffer = Buffer . from ( content );
}
const base64Content = contentBuffer . toString ( "base64" );
2025-09-12 14:42:00 -05:00
const escapedPath = filePath . replace ( /'/g , "'\"'\"'" );
const writeCommand = `echo ' ${ base64Content } ' | base64 -d > ' ${ escapedPath } ' && echo "SUCCESS"` ;
sshConn . client . exec ( writeCommand , ( err , stream ) => {
if ( err ) {
fileLogger . error ( "Fallback write command failed:" , err );
if ( ! res . headersSent ) {
return res . status ( 500 ). json ({
error : `Write failed: ${ err . message } ` ,
toast : { type : "error" , message : `Write failed: ${ err . message } ` },
});
}
return ;
}
let outputData = "" ;
let errorData = "" ;
stream . on ( "data" , ( chunk : Buffer ) => {
outputData += chunk . toString ();
});
stream . stderr . on ( "data" , ( chunk : Buffer ) => {
errorData += chunk . toString ();
});
stream . on ( "close" , ( code ) => {
if ( outputData . includes ( "SUCCESS" )) {
2025-08-17 22:57:25 -05:00
if ( ! res . headersSent ) {
2025-09-12 14:42:00 -05:00
res . json ({
message : "File written successfully" ,
path : filePath ,
toast : {
type : "success" ,
message : `File written: ${ filePath } ` ,
},
});
2025-08-17 22:57:25 -05:00
}
2025-09-12 14:42:00 -05:00
} else {
fileLogger . error (
`Fallback write failed with code ${ code } : ${ errorData } ` ,
);
if ( ! res . headersSent ) {
res . status ( 500 ). json ({
error : `Write failed: ${ errorData } ` ,
toast : { type : "error" , message : `Write failed: ${ errorData } ` },
});
}
}
});
2025-08-17 22:57:25 -05:00
2025-09-12 14:42:00 -05:00
stream . on ( "error" , ( streamErr ) => {
fileLogger . error ( "Fallback write stream error:" , streamErr );
if ( ! res . headersSent ) {
res
. status ( 500 )
. json ({ error : `Write stream error: ${ streamErr . message } ` });
}
});
});
} catch ( fallbackErr ) {
fileLogger . error ( "Fallback method failed:" , fallbackErr );
if ( ! res . headersSent ) {
res
. status ( 500 )
. json ({ error : `All write methods failed: ${ fallbackErr . message } ` });
}
}
};
trySFTP ();
2025-08-17 22:57:25 -05:00
});
2025-09-12 14:42:00 -05:00
app . post ( "/ssh/file_manager/ssh/uploadFile" , async ( req , res ) => {
2025-11-05 10:36:16 -06:00
const { sessionId , path : filePath , content , fileName } = req . body ;
2025-09-12 14:42:00 -05:00
const sshConn = sshSessions [ sessionId ];
2025-08-17 22:57:25 -05:00
2025-09-12 14:42:00 -05:00
if ( ! sessionId ) {
return res . status ( 400 ). json ({ error : "Session ID is required" });
}
2025-08-17 22:57:25 -05:00
2025-09-12 14:42:00 -05:00
if ( ! sshConn ? . isConnected ) {
return res . status ( 400 ). json ({ error : "SSH connection not established" });
}
2025-08-17 22:57:25 -05:00
2025-09-12 14:42:00 -05:00
if ( ! filePath || ! fileName || content === undefined ) {
return res
. status ( 400 )
. json ({ error : "File path, name, and content are required" });
}
2025-08-17 22:57:25 -05:00
2025-09-12 14:42:00 -05:00
sshConn . lastActive = Date . now ();
2025-08-17 22:57:25 -05:00
2025-10-01 15:40:10 -05:00
const contentSize =
typeof content === "string"
? Buffer . byteLength ( content , "utf8" )
: content . length ;
2025-09-12 14:42:00 -05:00
const fullPath = filePath . endsWith ( "/" )
? filePath + fileName
: filePath + "/" + fileName ;
2025-08-17 22:57:25 -05:00
2025-09-12 14:42:00 -05:00
const trySFTP = () => {
try {
sshConn . client . sftp (( err , sftp ) => {
if ( err ) {
fileLogger . warn (
`SFTP failed, trying fallback method: ${ err . message } ` ,
);
tryFallbackMethod ();
return ;
2025-08-17 22:57:25 -05:00
}
2025-09-12 14:42:00 -05:00
let fileBuffer ;
2025-08-17 22:57:25 -05:00
try {
2025-09-12 14:42:00 -05:00
if ( typeof content === "string" ) {
2025-12-31 22:20:12 -06:00
fileBuffer = Buffer . from ( content , "base64" );
2025-09-12 14:42:00 -05:00
} else if ( Buffer . isBuffer ( content )) {
fileBuffer = content ;
} else {
fileBuffer = Buffer . from ( content );
}
} catch ( bufferErr ) {
fileLogger . error ( "Buffer conversion error:" , bufferErr );
if ( ! res . headersSent ) {
return res
. status ( 500 )
. json ({ error : "Invalid file content format" });
}
return ;
}
2025-08-18 00:13:21 -05:00
2025-09-12 14:42:00 -05:00
const writeStream = sftp . createWriteStream ( fullPath );
let hasError = false ;
let hasFinished = false ;
writeStream . on ( "error" , ( streamErr ) => {
if ( hasError || hasFinished ) return ;
hasError = true ;
fileLogger . warn (
`SFTP write failed, trying fallback method: ${ streamErr . message } ` ,
2025-10-01 15:40:10 -05:00
{
operation : "file_upload" ,
sessionId ,
fileName ,
fileSize : contentSize ,
error : streamErr.message ,
},
2025-09-12 14:42:00 -05:00
);
tryFallbackMethod ();
});
writeStream . on ( "finish" , () => {
if ( hasError || hasFinished ) return ;
hasFinished = true ;
if ( ! res . headersSent ) {
res . json ({
message : "File uploaded successfully" ,
path : fullPath ,
toast : { type : "success" , message : `File uploaded: ${ fullPath } ` },
});
}
});
writeStream . on ( "close" , () => {
if ( hasError || hasFinished ) return ;
hasFinished = true ;
if ( ! res . headersSent ) {
res . json ({
message : "File uploaded successfully" ,
path : fullPath ,
toast : { type : "success" , message : `File uploaded: ${ fullPath } ` },
});
}
});
try {
writeStream . write ( fileBuffer );
writeStream . end ();
} catch ( writeErr ) {
if ( hasError || hasFinished ) return ;
hasError = true ;
fileLogger . warn (
`SFTP write operation failed, trying fallback method: ${ writeErr . message } ` ,
);
tryFallbackMethod ();
}
});
} catch ( sftpErr ) {
fileLogger . warn (
`SFTP connection error, trying fallback method: ${ sftpErr . message } ` ,
);
tryFallbackMethod ();
}
};
const tryFallbackMethod = () => {
try {
2025-12-31 22:20:12 -06:00
let contentBuffer : Buffer ;
if ( typeof content === "string" ) {
try {
contentBuffer = Buffer . from ( content , "base64" );
if ( contentBuffer . toString ( "base64" ) !== content ) {
contentBuffer = Buffer . from ( content , "utf8" );
}
} catch {
contentBuffer = Buffer . from ( content , "utf8" );
}
} else if ( Buffer . isBuffer ( content )) {
contentBuffer = content ;
} else {
contentBuffer = Buffer . from ( content );
}
const base64Content = contentBuffer . toString ( "base64" );
2025-09-12 14:42:00 -05:00
const chunkSize = 1000000 ;
const chunks = [];
for ( let i = 0 ; i < base64Content . length ; i += chunkSize ) {
chunks . push ( base64Content . slice ( i , i + chunkSize ));
}
if ( chunks . length === 1 ) {
const escapedPath = fullPath . replace ( /'/g , "'\"'\"'" );
const writeCommand = `echo ' ${ chunks [ 0 ] } ' | base64 -d > ' ${ escapedPath } ' && echo "SUCCESS"` ;
sshConn . client . exec ( writeCommand , ( err , stream ) => {
if ( err ) {
fileLogger . error ( "Fallback upload command failed:" , err );
if ( ! res . headersSent ) {
return res
. status ( 500 )
. json ({ error : `Upload failed: ${ err . message } ` });
2025-08-17 22:57:25 -05:00
}
2025-09-12 14:42:00 -05:00
return ;
}
2025-08-17 22:57:25 -05:00
2025-09-12 14:42:00 -05:00
let outputData = "" ;
let errorData = "" ;
2025-08-17 22:57:25 -05:00
2025-09-12 14:42:00 -05:00
stream . on ( "data" , ( chunk : Buffer ) => {
outputData += chunk . toString ();
});
2025-08-18 00:13:21 -05:00
2025-09-12 14:42:00 -05:00
stream . stderr . on ( "data" , ( chunk : Buffer ) => {
errorData += chunk . toString ();
});
2025-08-17 22:57:25 -05:00
2025-09-12 14:42:00 -05:00
stream . on ( "close" , ( code ) => {
if ( outputData . includes ( "SUCCESS" )) {
if ( ! res . headersSent ) {
res . json ({
message : "File uploaded successfully" ,
path : fullPath ,
toast : {
type : "success" ,
message : `File uploaded: ${ fullPath } ` ,
},
2025-08-17 22:57:25 -05:00
});
2025-09-12 14:42:00 -05:00
}
2025-08-17 22:57:25 -05:00
} else {
2025-09-12 14:42:00 -05:00
fileLogger . error (
`Fallback upload failed with code ${ code } : ${ errorData } ` ,
);
if ( ! res . headersSent ) {
res . status ( 500 ). json ({
error : `Upload failed: ${ errorData } ` ,
toast : {
type : "error" ,
message : `Upload failed: ${ errorData } ` ,
},
2025-08-17 22:57:25 -05:00
});
2025-09-12 14:42:00 -05:00
}
}
});
2025-08-18 00:13:21 -05:00
2025-09-12 14:42:00 -05:00
stream . on ( "error" , ( streamErr ) => {
fileLogger . error ( "Fallback upload stream error:" , streamErr );
if ( ! res . headersSent ) {
res
. status ( 500 )
. json ({ error : `Upload stream error: ${ streamErr . message } ` });
}
});
});
} else {
const escapedPath = fullPath . replace ( /'/g , "'\"'\"'" );
2025-08-17 22:57:25 -05:00
2025-09-12 14:42:00 -05:00
let writeCommand = `> ' ${ escapedPath } '` ;
2025-08-17 22:57:25 -05:00
2025-11-05 10:36:16 -06:00
chunks . forEach (( chunk ) => {
2025-09-12 14:42:00 -05:00
writeCommand += ` && echo ' ${ chunk } ' | base64 -d >> ' ${ escapedPath } '` ;
});
2025-08-17 22:57:25 -05:00
2025-09-12 14:42:00 -05:00
writeCommand += ` && echo "SUCCESS"` ;
2025-08-17 22:57:25 -05:00
2025-09-12 14:42:00 -05:00
sshConn . client . exec ( writeCommand , ( err , stream ) => {
if ( err ) {
fileLogger . error ( "Chunked fallback upload failed:" , err );
if ( ! res . headersSent ) {
return res
. status ( 500 )
. json ({ error : `Chunked upload failed: ${ err . message } ` });
}
return ;
}
2025-08-17 22:57:25 -05:00
2025-09-12 14:42:00 -05:00
let outputData = "" ;
let errorData = "" ;
2025-08-18 00:13:21 -05:00
2025-09-12 14:42:00 -05:00
stream . on ( "data" , ( chunk : Buffer ) => {
outputData += chunk . toString ();
});
2025-08-17 22:57:25 -05:00
2025-09-12 14:42:00 -05:00
stream . stderr . on ( "data" , ( chunk : Buffer ) => {
errorData += chunk . toString ();
});
stream . on ( "close" , ( code ) => {
if ( outputData . includes ( "SUCCESS" )) {
if ( ! res . headersSent ) {
res . json ({
message : "File uploaded successfully" ,
path : fullPath ,
toast : {
type : "success" ,
message : `File uploaded: ${ fullPath } ` ,
},
2025-08-17 22:57:25 -05:00
});
2025-09-12 14:42:00 -05:00
}
} else {
fileLogger . error (
`Chunked fallback upload failed with code ${ code } : ${ errorData } ` ,
);
if ( ! res . headersSent ) {
res . status ( 500 ). json ({
error : `Chunked upload failed: ${ errorData } ` ,
toast : {
type : "error" ,
message : `Chunked upload failed: ${ errorData } ` ,
},
});
}
2025-08-17 22:57:25 -05:00
}
2025-09-12 14:42:00 -05:00
});
2025-08-17 22:57:25 -05:00
2025-09-12 14:42:00 -05:00
stream . on ( "error" , ( streamErr ) => {
fileLogger . error (
"Chunked fallback upload stream error:" ,
streamErr ,
);
if ( ! res . headersSent ) {
res . status ( 500 ). json ({
error : `Chunked upload stream error: ${ streamErr . message } ` ,
});
}
});
});
}
} catch ( fallbackErr ) {
fileLogger . error ( "Fallback method failed:" , fallbackErr );
if ( ! res . headersSent ) {
res
. status ( 500 )
. json ({ error : `All upload methods failed: ${ fallbackErr . message } ` });
}
}
};
trySFTP ();
2025-08-17 22:57:25 -05:00
});
2025-09-12 14:42:00 -05:00
app . post ( "/ssh/file_manager/ssh/createFile" , async ( req , res ) => {
2025-11-05 10:36:16 -06:00
const { sessionId , path : filePath , fileName } = req . body ;
2025-09-12 14:42:00 -05:00
const sshConn = sshSessions [ sessionId ];
2025-08-17 22:57:25 -05:00
2025-09-12 14:42:00 -05:00
if ( ! sessionId ) {
return res . status ( 400 ). json ({ error : "Session ID is required" });
}
if ( ! sshConn ? . isConnected ) {
return res . status ( 400 ). json ({ error : "SSH connection not established" });
}
if ( ! filePath || ! fileName ) {
return res . status ( 400 ). json ({ error : "File path and name are required" });
}
sshConn . lastActive = Date . now ();
const fullPath = filePath . endsWith ( "/" )
? filePath + fileName
: filePath + "/" + fileName ;
const escapedPath = fullPath . replace ( /'/g , "'\"'\"'" );
const createCommand = `touch ' ${ escapedPath } ' && echo "SUCCESS" && exit 0` ;
sshConn . client . exec ( createCommand , ( err , stream ) => {
if ( err ) {
fileLogger . error ( "SSH createFile error:" , err );
if ( ! res . headersSent ) {
return res . status ( 500 ). json ({ error : err.message });
}
return ;
2025-08-17 22:57:25 -05:00
}
2025-09-12 14:42:00 -05:00
let outputData = "" ;
let errorData = "" ;
2025-08-17 22:57:25 -05:00
2025-09-12 14:42:00 -05:00
stream . on ( "data" , ( chunk : Buffer ) => {
outputData += chunk . toString ();
2025-08-17 22:57:25 -05:00
});
2025-09-12 14:42:00 -05:00
stream . stderr . on ( "data" , ( chunk : Buffer ) => {
errorData += chunk . toString ();
2025-08-17 22:57:25 -05:00
2025-09-12 14:42:00 -05:00
if ( chunk . toString (). includes ( "Permission denied" )) {
fileLogger . error ( `Permission denied creating file: ${ fullPath } ` );
if ( ! res . headersSent ) {
return res . status ( 403 ). json ({
error : `Permission denied: Cannot create file ${ fullPath } . Check directory permissions.` ,
});
2025-08-17 22:57:25 -05:00
}
2025-09-12 14:42:00 -05:00
return ;
}
2025-08-17 22:57:25 -05:00
});
2025-09-12 14:42:00 -05:00
stream . on ( "close" , ( code ) => {
if ( outputData . includes ( "SUCCESS" )) {
if ( ! res . headersSent ) {
res . json ({
message : "File created successfully" ,
path : fullPath ,
toast : { type : "success" , message : `File created: ${ fullPath } ` },
});
2025-08-17 22:57:25 -05:00
}
2025-09-12 14:42:00 -05:00
return ;
}
2025-08-17 22:57:25 -05:00
2025-09-12 14:42:00 -05:00
if ( code !== 0 ) {
fileLogger . error (
`SSH createFile command failed with code ${ code } : ${ errorData . replace ( /\n/g , " " ). trim () } ` ,
);
if ( ! res . headersSent ) {
return res . status ( 500 ). json ({
error : `Command failed: ${ errorData } ` ,
toast : {
type : "error" ,
message : `File creation failed: ${ errorData } ` ,
},
});
2025-08-17 22:57:25 -05:00
}
2025-09-12 14:42:00 -05:00
return ;
}
2025-08-17 22:57:25 -05:00
2025-09-12 14:42:00 -05:00
if ( ! res . headersSent ) {
res . json ({
message : "File created successfully" ,
path : fullPath ,
toast : { type : "success" , message : `File created: ${ fullPath } ` },
2025-08-07 02:20:27 -05:00
});
2025-09-12 14:42:00 -05:00
}
2025-08-07 02:20:27 -05:00
});
2025-09-12 14:42:00 -05:00
stream . on ( "error" , ( streamErr ) => {
fileLogger . error ( "SSH createFile stream error:" , streamErr );
if ( ! res . headersSent ) {
res . status ( 500 ). json ({ error : `Stream error: ${ streamErr . message } ` });
}
});
});
2025-08-07 02:20:27 -05:00
});
2025-09-12 14:42:00 -05:00
app . post ( "/ssh/file_manager/ssh/createFolder" , async ( req , res ) => {
2025-11-05 10:36:16 -06:00
const { sessionId , path : folderPath , folderName } = req . body ;
2025-09-12 14:42:00 -05:00
const sshConn = sshSessions [ sessionId ];
if ( ! sessionId ) {
return res . status ( 400 ). json ({ error : "Session ID is required" });
}
if ( ! sshConn ? . isConnected ) {
return res . status ( 400 ). json ({ error : "SSH connection not established" });
}
if ( ! folderPath || ! folderName ) {
return res . status ( 400 ). json ({ error : "Folder path and name are required" });
}
sshConn . lastActive = Date . now ();
const fullPath = folderPath . endsWith ( "/" )
? folderPath + folderName
: folderPath + "/" + folderName ;
const escapedPath = fullPath . replace ( /'/g , "'\"'\"'" );
const createCommand = `mkdir -p ' ${ escapedPath } ' && echo "SUCCESS" && exit 0` ;
sshConn . client . exec ( createCommand , ( err , stream ) => {
if ( err ) {
fileLogger . error ( "SSH createFolder error:" , err );
if ( ! res . headersSent ) {
return res . status ( 500 ). json ({ error : err.message });
}
return ;
}
let outputData = "" ;
let errorData = "" ;
stream . on ( "data" , ( chunk : Buffer ) => {
outputData += chunk . toString ();
});
stream . stderr . on ( "data" , ( chunk : Buffer ) => {
errorData += chunk . toString ();
if ( chunk . toString (). includes ( "Permission denied" )) {
fileLogger . error ( `Permission denied creating folder: ${ fullPath } ` );
if ( ! res . headersSent ) {
return res . status ( 403 ). json ({
error : `Permission denied: Cannot create folder ${ fullPath } . Check directory permissions.` ,
});
}
return ;
}
});
stream . on ( "close" , ( code ) => {
if ( outputData . includes ( "SUCCESS" )) {
if ( ! res . headersSent ) {
res . json ({
message : "Folder created successfully" ,
path : fullPath ,
toast : { type : "success" , message : `Folder created: ${ fullPath } ` },
});
}
return ;
}
if ( code !== 0 ) {
fileLogger . error (
`SSH createFolder command failed with code ${ code } : ${ errorData . replace ( /\n/g , " " ). trim () } ` ,
);
if ( ! res . headersSent ) {
return res . status ( 500 ). json ({
error : `Command failed: ${ errorData } ` ,
toast : {
type : "error" ,
message : `Folder creation failed: ${ errorData } ` ,
},
});
}
return ;
}
if ( ! res . headersSent ) {
res . json ({
message : "Folder created successfully" ,
path : fullPath ,
toast : { type : "success" , message : `Folder created: ${ fullPath } ` },
});
}
});
stream . on ( "error" , ( streamErr ) => {
fileLogger . error ( "SSH createFolder stream error:" , streamErr );
if ( ! res . headersSent ) {
res . status ( 500 ). json ({ error : `Stream error: ${ streamErr . message } ` });
}
});
});
2025-08-07 02:20:27 -05:00
});
2025-09-12 14:42:00 -05:00
app . delete ( "/ssh/file_manager/ssh/deleteItem" , async ( req , res ) => {
2025-11-05 10:36:16 -06:00
const { sessionId , path : itemPath , isDirectory } = req . body ;
2025-09-12 14:42:00 -05:00
const sshConn = sshSessions [ sessionId ];
if ( ! sessionId ) {
return res . status ( 400 ). json ({ error : "Session ID is required" });
}
if ( ! sshConn ? . isConnected ) {
return res . status ( 400 ). json ({ error : "SSH connection not established" });
}
if ( ! itemPath ) {
return res . status ( 400 ). json ({ error : "Item path is required" });
}
sshConn . lastActive = Date . now ();
const escapedPath = itemPath . replace ( /'/g , "'\"'\"'" );
const deleteCommand = isDirectory
? `rm -rf ' ${ escapedPath } ' && echo "SUCCESS" && exit 0`
: `rm -f ' ${ escapedPath } ' && echo "SUCCESS" && exit 0` ;
sshConn . client . exec ( deleteCommand , ( err , stream ) => {
if ( err ) {
fileLogger . error ( "SSH deleteItem error:" , err );
if ( ! res . headersSent ) {
return res . status ( 500 ). json ({ error : err.message });
}
return ;
}
let outputData = "" ;
let errorData = "" ;
stream . on ( "data" , ( chunk : Buffer ) => {
outputData += chunk . toString ();
});
stream . stderr . on ( "data" , ( chunk : Buffer ) => {
errorData += chunk . toString ();
if ( chunk . toString (). includes ( "Permission denied" )) {
fileLogger . error ( `Permission denied deleting: ${ itemPath } ` );
if ( ! res . headersSent ) {
return res . status ( 403 ). json ({
error : `Permission denied: Cannot delete ${ itemPath } . Check file permissions.` ,
});
}
return ;
}
});
stream . on ( "close" , ( code ) => {
if ( outputData . includes ( "SUCCESS" )) {
if ( ! res . headersSent ) {
res . json ({
message : "Item deleted successfully" ,
path : itemPath ,
toast : {
type : "success" ,
message : ` ${ isDirectory ? "Directory" : "File" } deleted: ${ itemPath } ` ,
},
});
}
return ;
}
if ( code !== 0 ) {
fileLogger . error (
`SSH deleteItem command failed with code ${ code } : ${ errorData . replace ( /\n/g , " " ). trim () } ` ,
);
if ( ! res . headersSent ) {
return res . status ( 500 ). json ({
error : `Command failed: ${ errorData } ` ,
toast : { type : "error" , message : `Delete failed: ${ errorData } ` },
});
}
return ;
}
if ( ! res . headersSent ) {
res . json ({
message : "Item deleted successfully" ,
path : itemPath ,
toast : {
type : "success" ,
message : ` ${ isDirectory ? "Directory" : "File" } deleted: ${ itemPath } ` ,
},
});
}
});
stream . on ( "error" , ( streamErr ) => {
fileLogger . error ( "SSH deleteItem stream error:" , streamErr );
if ( ! res . headersSent ) {
res . status ( 500 ). json ({ error : `Stream error: ${ streamErr . message } ` });
}
});
});
});
app . put ( "/ssh/file_manager/ssh/renameItem" , async ( req , res ) => {
2025-11-05 10:36:16 -06:00
const { sessionId , oldPath , newName } = req . body ;
2025-09-12 14:42:00 -05:00
const sshConn = sshSessions [ sessionId ];
if ( ! sessionId ) {
return res . status ( 400 ). json ({ error : "Session ID is required" });
}
if ( ! sshConn ? . isConnected ) {
return res . status ( 400 ). json ({ error : "SSH connection not established" });
}
if ( ! oldPath || ! newName ) {
return res
. status ( 400 )
. json ({ error : "Old path and new name are required" });
}
sshConn . lastActive = Date . now ();
const oldDir = oldPath . substring ( 0 , oldPath . lastIndexOf ( "/" ) + 1 );
const newPath = oldDir + newName ;
const escapedOldPath = oldPath . replace ( /'/g , "'\"'\"'" );
const escapedNewPath = newPath . replace ( /'/g , "'\"'\"'" );
const renameCommand = `mv ' ${ escapedOldPath } ' ' ${ escapedNewPath } ' && echo "SUCCESS" && exit 0` ;
sshConn . client . exec ( renameCommand , ( err , stream ) => {
if ( err ) {
fileLogger . error ( "SSH renameItem error:" , err );
if ( ! res . headersSent ) {
return res . status ( 500 ). json ({ error : err.message });
}
return ;
}
let outputData = "" ;
let errorData = "" ;
stream . on ( "data" , ( chunk : Buffer ) => {
outputData += chunk . toString ();
});
stream . stderr . on ( "data" , ( chunk : Buffer ) => {
errorData += chunk . toString ();
if ( chunk . toString (). includes ( "Permission denied" )) {
fileLogger . error ( `Permission denied renaming: ${ oldPath } ` );
if ( ! res . headersSent ) {
return res . status ( 403 ). json ({
error : `Permission denied: Cannot rename ${ oldPath } . Check file permissions.` ,
});
}
return ;
}
});
stream . on ( "close" , ( code ) => {
if ( outputData . includes ( "SUCCESS" )) {
if ( ! res . headersSent ) {
res . json ({
message : "Item renamed successfully" ,
oldPath ,
newPath ,
toast : {
type : "success" ,
message : `Item renamed: ${ oldPath } -> ${ newPath } ` ,
},
});
}
return ;
}
if ( code !== 0 ) {
fileLogger . error (
`SSH renameItem command failed with code ${ code } : ${ errorData . replace ( /\n/g , " " ). trim () } ` ,
);
if ( ! res . headersSent ) {
return res . status ( 500 ). json ({
error : `Command failed: ${ errorData } ` ,
toast : { type : "error" , message : `Rename failed: ${ errorData } ` },
});
}
return ;
}
if ( ! res . headersSent ) {
res . json ({
message : "Item renamed successfully" ,
oldPath ,
newPath ,
toast : {
type : "success" ,
message : `Item renamed: ${ oldPath } -> ${ newPath } ` ,
},
});
}
});
stream . on ( "error" , ( streamErr ) => {
fileLogger . error ( "SSH renameItem stream error:" , streamErr );
if ( ! res . headersSent ) {
res . status ( 500 ). json ({ error : `Stream error: ${ streamErr . message } ` });
}
});
});
});
2025-10-01 15:40:10 -05:00
app . put ( "/ssh/file_manager/ssh/moveItem" , async ( req , res ) => {
2025-11-05 10:36:16 -06:00
const { sessionId , oldPath , newPath } = req . body ;
2025-10-01 15:40:10 -05:00
const sshConn = sshSessions [ sessionId ];
if ( ! sessionId ) {
return res . status ( 400 ). json ({ error : "Session ID is required" });
}
if ( ! sshConn ? . isConnected ) {
return res . status ( 400 ). json ({ error : "SSH connection not established" });
}
if ( ! oldPath || ! newPath ) {
return res
. status ( 400 )
. json ({ error : "Old path and new path are required" });
}
sshConn . lastActive = Date . now ();
const escapedOldPath = oldPath . replace ( /'/g , "'\"'\"'" );
const escapedNewPath = newPath . replace ( /'/g , "'\"'\"'" );
const moveCommand = `mv ' ${ escapedOldPath } ' ' ${ escapedNewPath } ' && echo "SUCCESS" && exit 0` ;
const commandTimeout = setTimeout (() => {
if ( ! res . headersSent ) {
res . status ( 408 ). json ({
error : "Move operation timed out. SSH connection may be unstable." ,
toast : {
type : "error" ,
message : "Move operation timed out. SSH connection may be unstable." ,
},
});
}
}, 60000 );
sshConn . client . exec ( moveCommand , ( err , stream ) => {
if ( err ) {
clearTimeout ( commandTimeout );
fileLogger . error ( "SSH moveItem error:" , err );
if ( ! res . headersSent ) {
return res . status ( 500 ). json ({ error : err.message });
}
return ;
}
let outputData = "" ;
let errorData = "" ;
stream . on ( "data" , ( chunk : Buffer ) => {
outputData += chunk . toString ();
});
stream . stderr . on ( "data" , ( chunk : Buffer ) => {
errorData += chunk . toString ();
if ( chunk . toString (). includes ( "Permission denied" )) {
fileLogger . error ( `Permission denied moving: ${ oldPath } ` );
if ( ! res . headersSent ) {
return res . status ( 403 ). json ({
error : `Permission denied: Cannot move ${ oldPath } . Check file permissions.` ,
toast : {
type : "error" ,
message : `Permission denied: Cannot move ${ oldPath } . Check file permissions.` ,
},
});
}
return ;
}
});
stream . on ( "close" , ( code ) => {
clearTimeout ( commandTimeout );
if ( outputData . includes ( "SUCCESS" )) {
if ( ! res . headersSent ) {
res . json ({
message : "Item moved successfully" ,
oldPath ,
newPath ,
toast : {
type : "success" ,
message : `Item moved: ${ oldPath } -> ${ newPath } ` ,
},
});
}
return ;
}
if ( code !== 0 ) {
fileLogger . error (
`SSH moveItem command failed with code ${ code } : ${ errorData . replace ( /\n/g , " " ). trim () } ` ,
);
if ( ! res . headersSent ) {
return res . status ( 500 ). json ({
error : `Command failed: ${ errorData } ` ,
toast : { type : "error" , message : `Move failed: ${ errorData } ` },
});
}
return ;
}
if ( ! res . headersSent ) {
res . json ({
message : "Item moved successfully" ,
oldPath ,
newPath ,
toast : {
type : "success" ,
message : `Item moved: ${ oldPath } -> ${ newPath } ` ,
},
});
}
});
stream . on ( "error" , ( streamErr ) => {
clearTimeout ( commandTimeout );
fileLogger . error ( "SSH moveItem stream error:" , streamErr );
if ( ! res . headersSent ) {
res . status ( 500 ). json ({ error : `Stream error: ${ streamErr . message } ` });
}
});
});
});
app . post ( "/ssh/file_manager/ssh/downloadFile" , async ( req , res ) => {
const { sessionId , path : filePath , hostId , userId } = req . body ;
if ( ! sessionId || ! filePath ) {
fileLogger . warn ( "Missing download parameters" , {
operation : "file_download" ,
sessionId ,
hasFilePath : !! filePath ,
});
return res . status ( 400 ). json ({ error : "Missing download parameters" });
}
const sshConn = sshSessions [ sessionId ];
if ( ! sshConn || ! sshConn . isConnected ) {
fileLogger . warn ( "SSH session not found or not connected for download" , {
operation : "file_download" ,
sessionId ,
isConnected : sshConn?.isConnected ,
});
return res
. status ( 400 )
. json ({ error : "SSH session not found or not connected" });
}
sshConn . lastActive = Date . now ();
scheduleSessionCleanup ( sessionId );
sshConn . client . sftp (( err , sftp ) => {
if ( err ) {
fileLogger . error ( "SFTP connection failed for download:" , err );
return res . status ( 500 ). json ({ error : "SFTP connection failed" });
}
sftp . stat ( filePath , ( statErr , stats ) => {
if ( statErr ) {
fileLogger . error ( "File stat failed for download:" , statErr );
return res
. status ( 500 )
. json ({ error : `Cannot access file: ${ statErr . message } ` });
}
if ( ! stats . isFile ()) {
fileLogger . warn ( "Attempted to download non-file" , {
operation : "file_download" ,
sessionId ,
filePath ,
isFile : stats.isFile (),
isDirectory : stats.isDirectory (),
});
return res
. status ( 400 )
. json ({ error : "Cannot download directories or special files" });
}
const MAX_FILE_SIZE = 5 * 1024 * 1024 * 1024 ;
if ( stats . size > MAX_FILE_SIZE ) {
fileLogger . warn ( "File too large for download" , {
operation : "file_download" ,
sessionId ,
filePath ,
fileSize : stats.size ,
maxSize : MAX_FILE_SIZE ,
});
return res . status ( 400 ). json ({
error : `File too large. Maximum size is ${ MAX_FILE_SIZE / 1024 / 1024 } MB, file is ${ ( stats . size / 1024 / 1024 ). toFixed ( 2 ) } MB` ,
});
}
sftp . readFile ( filePath , ( readErr , data ) => {
if ( readErr ) {
fileLogger . error ( "File read failed for download:" , readErr );
return res
. status ( 500 )
. json ({ error : `Failed to read file: ${ readErr . message } ` });
}
const base64Content = data . toString ( "base64" );
const fileName = filePath . split ( "/" ). pop () || "download" ;
fileLogger . success ( "File downloaded successfully" , {
operation : "file_download" ,
sessionId ,
filePath ,
fileName ,
fileSize : stats.size ,
hostId ,
userId ,
});
res . json ({
content : base64Content ,
fileName : fileName ,
size : stats.size ,
mimeType : getMimeType ( fileName ),
path : filePath ,
});
});
});
});
});
app . post ( "/ssh/file_manager/ssh/copyItem" , async ( req , res ) => {
const { sessionId , sourcePath , targetDir , hostId , userId } = req . body ;
if ( ! sessionId || ! sourcePath || ! targetDir ) {
return res . status ( 400 ). json ({ error : "Missing required parameters" });
}
const sshConn = sshSessions [ sessionId ];
if ( ! sshConn || ! sshConn . isConnected ) {
return res
. status ( 400 )
. json ({ error : "SSH session not found or not connected" });
}
sshConn . lastActive = Date . now ();
scheduleSessionCleanup ( sessionId );
const sourceName = sourcePath . split ( "/" ). pop () || "copied_item" ;
const timestamp = Date . now (). toString (). slice ( - 8 );
const uniqueName = ` ${ sourceName } _copy_ ${ timestamp } ` ;
const targetPath = ` ${ targetDir } / ${ uniqueName } ` ;
const escapedSource = sourcePath . replace ( /'/g , "'\"'\"'" );
const escapedTarget = targetPath . replace ( /'/g , "'\"'\"'" );
const copyCommand = `cp ' ${ escapedSource } ' ' ${ escapedTarget } ' && echo "COPY_SUCCESS"` ;
const commandTimeout = setTimeout (() => {
fileLogger . error ( "Copy command timed out after 60 seconds" , {
sourcePath ,
targetPath ,
command : copyCommand ,
});
if ( ! res . headersSent ) {
res . status ( 500 ). json ({
error : "Copy operation timed out" ,
toast : {
type : "error" ,
message : "Copy operation timed out. SSH connection may be unstable." ,
},
});
}
}, 60000 );
sshConn . client . exec ( copyCommand , ( err , stream ) => {
if ( err ) {
clearTimeout ( commandTimeout );
fileLogger . error ( "SSH copyItem error:" , err );
if ( ! res . headersSent ) {
return res . status ( 500 ). json ({ error : err.message });
}
return ;
}
let errorData = "" ;
let stdoutData = "" ;
stream . on ( "data" , ( data : Buffer ) => {
const output = data . toString ();
stdoutData += output ;
stream . stderr . on ( "data" , ( data : Buffer ) => {
const output = data . toString ();
errorData += output ;
});
stream . on ( "close" , ( code ) => {
clearTimeout ( commandTimeout );
if ( code !== 0 ) {
const fullErrorInfo =
errorData || stdoutData || "No error message available" ;
fileLogger . error ( `SSH copyItem command failed with code ${ code } ` , {
operation : "file_copy_failed" ,
sessionId ,
sourcePath ,
targetPath ,
command : copyCommand ,
exitCode : code ,
errorData ,
stdoutData ,
fullErrorInfo ,
});
if ( ! res . headersSent ) {
return res . status ( 500 ). json ({
error : `Copy failed: ${ fullErrorInfo } ` ,
toast : {
type : "error" ,
message : `Copy failed: ${ fullErrorInfo } ` ,
},
debug : {
sourcePath ,
targetPath ,
exitCode : code ,
command : copyCommand ,
},
});
}
return ;
}
const copySuccessful =
stdoutData . includes ( "COPY_SUCCESS" ) || code === 0 ;
if ( copySuccessful ) {
fileLogger . success ( "Item copied successfully" , {
operation : "file_copy" ,
sessionId ,
sourcePath ,
targetPath ,
uniqueName ,
hostId ,
userId ,
});
if ( ! res . headersSent ) {
res . json ({
message : "Item copied successfully" ,
sourcePath ,
targetPath ,
uniqueName ,
toast : {
type : "success" ,
message : `Successfully copied to: ${ uniqueName } ` ,
},
});
}
} else {
fileLogger . warn ( "Copy completed but without success confirmation" , {
operation : "file_copy_uncertain" ,
sessionId ,
sourcePath ,
targetPath ,
code ,
stdoutData : stdoutData.substring ( 0 , 200 ),
});
if ( ! res . headersSent ) {
res . json ({
message : "Copy may have completed" ,
sourcePath ,
targetPath ,
uniqueName ,
toast : {
type : "warning" ,
message : `Copy completed but verification uncertain for: ${ uniqueName } ` ,
},
});
}
}
});
stream . on ( "error" , ( streamErr ) => {
clearTimeout ( commandTimeout );
fileLogger . error ( "SSH copyItem stream error:" , streamErr );
if ( ! res . headersSent ) {
res . status ( 500 ). json ({ error : `Stream error: ${ streamErr . message } ` });
}
});
});
});
});
app . post ( "/ssh/file_manager/ssh/executeFile" , async ( req , res ) => {
2025-11-05 10:36:16 -06:00
const { sessionId , filePath } = req . body ;
2025-10-01 15:40:10 -05:00
const sshConn = sshSessions [ sessionId ];
if ( ! sshConn || ! sshConn . isConnected ) {
fileLogger . error (
"SSH connection not found or not connected for executeFile" ,
{
operation : "execute_file" ,
sessionId ,
hasConnection : !! sshConn ,
isConnected : sshConn?.isConnected ,
},
);
return res . status ( 400 ). json ({ error : "SSH connection not available" });
}
if ( ! filePath ) {
return res . status ( 400 ). json ({ error : "File path is required" });
}
const escapedPath = filePath . replace ( /'/g , "'\"'\"'" );
const checkCommand = `test -x ' ${ escapedPath } ' && echo "EXECUTABLE" || echo "NOT_EXECUTABLE"` ;
sshConn . client . exec ( checkCommand , ( checkErr , checkStream ) => {
if ( checkErr ) {
fileLogger . error ( "SSH executeFile check error:" , checkErr );
return res
. status ( 500 )
. json ({ error : "Failed to check file executability" });
}
let checkResult = "" ;
checkStream . on ( "data" , ( data ) => {
checkResult += data . toString ();
});
2025-11-05 10:36:16 -06:00
checkStream . on ( "close" , () => {
2025-10-01 15:40:10 -05:00
if ( ! checkResult . includes ( "EXECUTABLE" )) {
return res . status ( 400 ). json ({ error : "File is not executable" });
}
const executeCommand = `cd "$(dirname ' ${ escapedPath } ')" && ' ${ escapedPath } ' 2>&1; echo "EXIT_CODE:$?"` ;
sshConn . client . exec ( executeCommand , ( err , stream ) => {
if ( err ) {
fileLogger . error ( "SSH executeFile error:" , err );
return res . status ( 500 ). json ({ error : "Failed to execute file" });
}
let output = "" ;
let errorOutput = "" ;
stream . on ( "data" , ( data ) => {
output += data . toString ();
});
stream . stderr . on ( "data" , ( data ) => {
errorOutput += data . toString ();
});
stream . on ( "close" , ( code ) => {
const exitCodeMatch = output . match ( /EXIT_CODE:(\d+)$/ );
const actualExitCode = exitCodeMatch
? parseInt ( exitCodeMatch [ 1 ])
: code ;
const cleanOutput = output . replace ( /EXIT_CODE:\d+$/ , "" ). trim ();
fileLogger . info ( "File execution completed" , {
operation : "execute_file" ,
sessionId ,
filePath ,
exitCode : actualExitCode ,
outputLength : cleanOutput.length ,
errorLength : errorOutput.length ,
});
res . json ({
success : true ,
exitCode : actualExitCode ,
output : cleanOutput ,
error : errorOutput ,
timestamp : new Date (). toISOString (),
});
});
stream . on ( "error" , ( streamErr ) => {
fileLogger . error ( "SSH executeFile stream error:" , streamErr );
if ( ! res . headersSent ) {
res . status ( 500 ). json ({ error : "Execution stream error" });
}
});
});
});
});
});
2025-11-17 09:46:05 -06:00
app . post ( "/ssh/file_manager/ssh/changePermissions" , async ( req , res ) => {
const { sessionId , path , permissions } = req . body ;
const sshConn = sshSessions [ sessionId ];
if ( ! sshConn || ! sshConn . isConnected ) {
fileLogger . error (
"SSH connection not found or not connected for changePermissions" ,
{
operation : "change_permissions" ,
sessionId ,
hasConnection : !! sshConn ,
isConnected : sshConn?.isConnected ,
},
);
return res . status ( 400 ). json ({ error : "SSH connection not available" });
}
if ( ! path ) {
return res . status ( 400 ). json ({ error : "File path is required" });
}
if ( ! permissions || ! /^\d{3,4}$/ . test ( permissions )) {
return res . status ( 400 ). json ({
error : "Valid permissions required (e.g., 755, 644)" ,
});
}
sshConn . lastActive = Date . now ();
scheduleSessionCleanup ( sessionId );
const octalPerms = permissions . slice ( - 3 );
const escapedPath = path . replace ( /'/g , "'\"'\"'" );
const command = `chmod ${ octalPerms } ' ${ escapedPath } ' && echo "SUCCESS"` ;
fileLogger . info ( "Changing file permissions" , {
operation : "change_permissions" ,
sessionId ,
path ,
permissions : octalPerms ,
});
const commandTimeout = setTimeout (() => {
if ( ! res . headersSent ) {
fileLogger . error ( "changePermissions command timeout" , {
operation : "change_permissions" ,
sessionId ,
path ,
permissions : octalPerms ,
});
res . status ( 408 ). json ({
error : "Permission change timed out. SSH connection may be unstable." ,
});
}
}, 10000 );
sshConn . client . exec ( command , ( err , stream ) => {
if ( err ) {
clearTimeout ( commandTimeout );
fileLogger . error ( "SSH changePermissions exec error:" , err , {
operation : "change_permissions" ,
sessionId ,
path ,
permissions : octalPerms ,
});
if ( ! res . headersSent ) {
return res . status ( 500 ). json ({ error : "Failed to change permissions" });
}
return ;
}
let outputData = "" ;
let errorOutput = "" ;
stream . on ( "data" , ( chunk : Buffer ) => {
outputData += chunk . toString ();
});
stream . stderr . on ( "data" , ( data : Buffer ) => {
errorOutput += data . toString ();
});
stream . on ( "close" , ( code ) => {
clearTimeout ( commandTimeout );
if ( outputData . includes ( "SUCCESS" )) {
fileLogger . success ( "File permissions changed successfully" , {
operation : "change_permissions" ,
sessionId ,
path ,
permissions : octalPerms ,
});
if ( ! res . headersSent ) {
res . json ({
success : true ,
message : "Permissions changed successfully" ,
});
}
return ;
}
if ( code !== 0 ) {
fileLogger . error ( "chmod command failed" , {
operation : "change_permissions" ,
sessionId ,
path ,
permissions : octalPerms ,
exitCode : code ,
error : errorOutput ,
});
if ( ! res . headersSent ) {
return res . status ( 500 ). json ({
error : errorOutput || "Failed to change permissions" ,
});
}
return ;
}
fileLogger . success ( "File permissions changed successfully" , {
operation : "change_permissions" ,
sessionId ,
path ,
permissions : octalPerms ,
});
if ( ! res . headersSent ) {
res . json ({
success : true ,
message : "Permissions changed successfully" ,
});
}
});
stream . on ( "error" , ( streamErr ) => {
clearTimeout ( commandTimeout );
fileLogger . error ( "SSH changePermissions stream error:" , streamErr , {
operation : "change_permissions" ,
sessionId ,
path ,
permissions : octalPerms ,
});
if ( ! res . headersSent ) {
res
. status ( 500 )
. json ({ error : "Stream error while changing permissions" });
}
});
});
});
// Route: Extract archive file (requires JWT)
// POST /ssh/file_manager/ssh/extractArchive
app . post ( "/ssh/file_manager/ssh/extractArchive" , async ( req , res ) => {
const { sessionId , archivePath , extractPath } = req . body ;
if ( ! sessionId || ! archivePath ) {
return res . status ( 400 ). json ({ error : "Missing required parameters" });
}
const session = sshSessions [ sessionId ];
if ( ! session || ! session . isConnected ) {
return res . status ( 400 ). json ({ error : "SSH session not connected" });
}
session . lastActive = Date . now ();
scheduleSessionCleanup ( sessionId );
const fileName = archivePath . split ( "/" ). pop () || "" ;
const fileExt = fileName . toLowerCase ();
let extractCommand = "" ;
const targetPath =
extractPath || archivePath . substring ( 0 , archivePath . lastIndexOf ( "/" ));
if ( fileExt . endsWith ( ".tar.gz" ) || fileExt . endsWith ( ".tgz" )) {
extractCommand = `tar -xzf " ${ archivePath } " -C " ${ targetPath } "` ;
} else if ( fileExt . endsWith ( ".tar.bz2" ) || fileExt . endsWith ( ".tbz2" )) {
extractCommand = `tar -xjf " ${ archivePath } " -C " ${ targetPath } "` ;
} else if ( fileExt . endsWith ( ".tar.xz" )) {
extractCommand = `tar -xJf " ${ archivePath } " -C " ${ targetPath } "` ;
} else if ( fileExt . endsWith ( ".tar" )) {
extractCommand = `tar -xf " ${ archivePath } " -C " ${ targetPath } "` ;
} else if ( fileExt . endsWith ( ".zip" )) {
extractCommand = `unzip -o " ${ archivePath } " -d " ${ targetPath } "` ;
} else if ( fileExt . endsWith ( ".gz" ) && ! fileExt . endsWith ( ".tar.gz" )) {
extractCommand = `gunzip -c " ${ archivePath } " > " ${ archivePath . replace ( /\.gz$/ , "" ) } "` ;
} else if ( fileExt . endsWith ( ".bz2" ) && ! fileExt . endsWith ( ".tar.bz2" )) {
extractCommand = `bunzip2 -k " ${ archivePath } "` ;
} else if ( fileExt . endsWith ( ".xz" ) && ! fileExt . endsWith ( ".tar.xz" )) {
extractCommand = `unxz -k " ${ archivePath } "` ;
} else if ( fileExt . endsWith ( ".7z" )) {
extractCommand = `7z x " ${ archivePath } " -o" ${ targetPath } "` ;
} else if ( fileExt . endsWith ( ".rar" )) {
extractCommand = `unrar x " ${ archivePath } " " ${ targetPath } /"` ;
} else {
return res . status ( 400 ). json ({ error : "Unsupported archive format" });
}
fileLogger . info ( "Extracting archive" , {
operation : "extract_archive" ,
sessionId ,
archivePath ,
extractPath : targetPath ,
command : extractCommand ,
});
session . client . exec ( extractCommand , ( err , stream ) => {
if ( err ) {
fileLogger . error ( "SSH exec error during extract:" , err , {
operation : "extract_archive" ,
sessionId ,
archivePath ,
});
return res
. status ( 500 )
. json ({ error : "Failed to execute extract command" });
}
let errorOutput = "" ;
2025-12-31 22:20:12 -06:00
stream . on ( "data" , ( data : Buffer ) => {});
2025-11-17 09:46:05 -06:00
stream . stderr . on ( "data" , ( data : Buffer ) => {
errorOutput += data . toString ();
});
stream . on ( "close" , ( code : number ) => {
if ( code !== 0 ) {
fileLogger . error ( "Extract command failed" , {
operation : "extract_archive" ,
sessionId ,
archivePath ,
exitCode : code ,
error : errorOutput ,
});
let friendlyError = errorOutput || "Failed to extract archive" ;
if (
errorOutput . includes ( "command not found" ) ||
errorOutput . includes ( "not found" )
) {
let missingCmd = "" ;
let installHint = "" ;
if ( fileExt . endsWith ( ".zip" )) {
missingCmd = "unzip" ;
installHint =
"apt install unzip / yum install unzip / brew install unzip" ;
} else if (
fileExt . endsWith ( ".tar.gz" ) ||
fileExt . endsWith ( ".tgz" ) ||
fileExt . endsWith ( ".tar.bz2" ) ||
fileExt . endsWith ( ".tbz2" ) ||
fileExt . endsWith ( ".tar.xz" ) ||
fileExt . endsWith ( ".tar" )
) {
missingCmd = "tar" ;
installHint = "Usually pre-installed on Linux/Unix systems" ;
} else if ( fileExt . endsWith ( ".gz" )) {
missingCmd = "gunzip" ;
installHint =
"apt install gzip / yum install gzip / Usually pre-installed" ;
} else if ( fileExt . endsWith ( ".bz2" )) {
missingCmd = "bunzip2" ;
installHint =
"apt install bzip2 / yum install bzip2 / brew install bzip2" ;
} else if ( fileExt . endsWith ( ".xz" )) {
missingCmd = "unxz" ;
installHint =
"apt install xz-utils / yum install xz / brew install xz" ;
} else if ( fileExt . endsWith ( ".7z" )) {
missingCmd = "7z" ;
installHint =
"apt install p7zip-full / yum install p7zip / brew install p7zip" ;
} else if ( fileExt . endsWith ( ".rar" )) {
missingCmd = "unrar" ;
installHint =
"apt install unrar / yum install unrar / brew install unrar" ;
}
if ( missingCmd ) {
friendlyError = `Command ' ${ missingCmd } ' not found on remote server. Please install it first: ${ installHint } ` ;
}
}
return res . status ( 500 ). json ({ error : friendlyError });
}
fileLogger . success ( "Archive extracted successfully" , {
operation : "extract_archive" ,
sessionId ,
archivePath ,
extractPath : targetPath ,
});
res . json ({
success : true ,
message : "Archive extracted successfully" ,
extractPath : targetPath ,
});
});
stream . on ( "error" , ( streamErr ) => {
fileLogger . error ( "SSH extractArchive stream error:" , streamErr , {
operation : "extract_archive" ,
sessionId ,
archivePath ,
});
if ( ! res . headersSent ) {
res
. status ( 500 )
. json ({ error : "Stream error while extracting archive" });
}
});
});
});
// Route: Compress files/folders (requires JWT)
// POST /ssh/file_manager/ssh/compressFiles
app . post ( "/ssh/file_manager/ssh/compressFiles" , async ( req , res ) => {
const { sessionId , paths , archiveName , format } = req . body ;
if (
! sessionId ||
! paths ||
! Array . isArray ( paths ) ||
paths . length === 0 ||
! archiveName
) {
return res . status ( 400 ). json ({ error : "Missing required parameters" });
}
const session = sshSessions [ sessionId ];
if ( ! session || ! session . isConnected ) {
return res . status ( 400 ). json ({ error : "SSH session not connected" });
}
session . lastActive = Date . now ();
scheduleSessionCleanup ( sessionId );
const compressionFormat = format || "zip" ;
let compressCommand = "" ;
const firstPath = paths [ 0 ];
const workingDir = firstPath . substring ( 0 , firstPath . lastIndexOf ( "/" )) || "/" ;
const fileNames = paths
. map (( p ) => {
const name = p . split ( "/" ). pop ();
return `" ${ name } "` ;
})
. join ( " " );
let archivePath = "" ;
if ( archiveName . includes ( "/" )) {
archivePath = archiveName ;
} else {
archivePath = workingDir . endsWith ( "/" )
? ` ${ workingDir }${ archiveName } `
: ` ${ workingDir } / ${ archiveName } ` ;
}
if ( compressionFormat === "zip" ) {
compressCommand = `cd " ${ workingDir } " && zip -r " ${ archivePath } " ${ fileNames } ` ;
} else if ( compressionFormat === "tar.gz" || compressionFormat === "tgz" ) {
compressCommand = `cd " ${ workingDir } " && tar -czf " ${ archivePath } " ${ fileNames } ` ;
} else if ( compressionFormat === "tar.bz2" || compressionFormat === "tbz2" ) {
compressCommand = `cd " ${ workingDir } " && tar -cjf " ${ archivePath } " ${ fileNames } ` ;
} else if ( compressionFormat === "tar.xz" ) {
compressCommand = `cd " ${ workingDir } " && tar -cJf " ${ archivePath } " ${ fileNames } ` ;
} else if ( compressionFormat === "tar" ) {
compressCommand = `cd " ${ workingDir } " && tar -cf " ${ archivePath } " ${ fileNames } ` ;
} else if ( compressionFormat === "7z" ) {
compressCommand = `cd " ${ workingDir } " && 7z a " ${ archivePath } " ${ fileNames } ` ;
} else {
return res . status ( 400 ). json ({ error : "Unsupported compression format" });
}
fileLogger . info ( "Compressing files" , {
operation : "compress_files" ,
sessionId ,
paths ,
archivePath ,
format : compressionFormat ,
command : compressCommand ,
});
session . client . exec ( compressCommand , ( err , stream ) => {
if ( err ) {
fileLogger . error ( "SSH exec error during compress:" , err , {
operation : "compress_files" ,
sessionId ,
paths ,
});
return res
. status ( 500 )
. json ({ error : "Failed to execute compress command" });
}
let errorOutput = "" ;
2025-12-31 22:20:12 -06:00
stream . on ( "data" , ( data : Buffer ) => {});
2025-11-17 09:46:05 -06:00
stream . stderr . on ( "data" , ( data : Buffer ) => {
errorOutput += data . toString ();
});
stream . on ( "close" , ( code : number ) => {
if ( code !== 0 ) {
fileLogger . error ( "Compress command failed" , {
operation : "compress_files" ,
sessionId ,
paths ,
archivePath ,
exitCode : code ,
error : errorOutput ,
});
let friendlyError = errorOutput || "Failed to compress files" ;
if (
errorOutput . includes ( "command not found" ) ||
errorOutput . includes ( "not found" )
) {
const commandMap : Record < string , { cmd : string ; install : string }> = {
zip : {
cmd : "zip" ,
install : "apt install zip / yum install zip / brew install zip" ,
},
"tar.gz" : {
cmd : "tar" ,
install : "Usually pre-installed on Linux/Unix systems" ,
},
"tar.bz2" : {
cmd : "tar" ,
install : "Usually pre-installed on Linux/Unix systems" ,
},
"tar.xz" : {
cmd : "tar" ,
install : "Usually pre-installed on Linux/Unix systems" ,
},
tar : {
cmd : "tar" ,
install : "Usually pre-installed on Linux/Unix systems" ,
},
"7z" : {
cmd : "7z" ,
install :
"apt install p7zip-full / yum install p7zip / brew install p7zip" ,
},
};
const info = commandMap [ compressionFormat ];
if ( info ) {
friendlyError = `Command ' ${ info . cmd } ' not found on remote server. Please install it first: ${ info . install } ` ;
}
}
return res . status ( 500 ). json ({ error : friendlyError });
}
fileLogger . success ( "Files compressed successfully" , {
operation : "compress_files" ,
sessionId ,
paths ,
archivePath ,
format : compressionFormat ,
});
res . json ({
success : true ,
message : "Files compressed successfully" ,
archivePath : archivePath ,
});
});
stream . on ( "error" , ( streamErr ) => {
fileLogger . error ( "SSH compressFiles stream error:" , streamErr , {
operation : "compress_files" ,
sessionId ,
paths ,
});
if ( ! res . headersSent ) {
res . status ( 500 ). json ({ error : "Stream error while compressing files" });
}
});
});
});
2025-09-12 14:42:00 -05:00
process . on ( "SIGINT" , () => {
Object . keys ( sshSessions ). forEach ( cleanupSession );
process . exit ( 0 );
});
process . on ( "SIGTERM" , () => {
Object . keys ( sshSessions ). forEach ( cleanupSession );
process . exit ( 0 );
2025-08-07 02:20:27 -05:00
});
2025-10-01 15:40:10 -05:00
const PORT = 30004 ;
try {
const server = app . listen ( PORT , async () => {
try {
await authManager . initialize ();
} catch ( err ) {
fileLogger . error ( "Failed to initialize AuthManager" , err , {
operation : "auth_init_error" ,
});
}
});
server . on ( "error" , ( err ) => {
fileLogger . error ( "File Manager server error" , err , {
operation : "file_manager_server_error" ,
port : PORT ,
});
});
} catch ( err ) {
fileLogger . error ( "Failed to start File Manager server" , err , {
operation : "file_manager_server_start_failed" ,
2025-09-12 14:42:00 -05:00
port : PORT ,
});
2025-10-01 15:40:10 -05:00
}