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" ;
2026-01-24 19:49:42 -06:00
import type { LogEntry , ConnectionStage } from "../../types/connection-log.js" ;
function createConnectionLog (
type : "info" | "success" | "warning" | "error" ,
stage : ConnectionStage ,
message : string ,
details? : Record < string , any >,
) : Omit < LogEntry , "id" | "timestamp" > {
return {
type ,
stage ,
message ,
details ,
};
}
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
2026-01-24 19:49:42 -06:00
function modeToPermissions ( mode : number ) : string {
const S_IFDIR = 0 o040000 ;
const S_IFLNK = 0 o120000 ;
const S_IFMT = 0 o170000 ;
const type = mode & S_IFMT ;
const prefix = type === S_IFDIR ? "d" : type === S_IFLNK ? "l" : "-" ;
const perms = [
mode & 0 o400 ? "r" : "-" ,
mode & 0 o200 ? "w" : "-" ,
mode & 0 o100 ? "x" : "-" ,
mode & 0 o040 ? "r" : "-" ,
mode & 0 o020 ? "w" : "-" ,
mode & 0 o010 ? "x" : "-" ,
mode & 0 o004 ? "r" : "-" ,
mode & 0 o002 ? "w" : "-" ,
mode & 0 o001 ? "x" : "-" ,
]. join ( "" );
return prefix + perms ;
}
function formatMtime ( mtime : number ) : string {
const date = new Date ( mtime * 1000 );
const months = [
"Jan" ,
"Feb" ,
"Mar" ,
"Apr" ,
"May" ,
"Jun" ,
"Jul" ,
"Aug" ,
"Sep" ,
"Oct" ,
"Nov" ,
"Dec" ,
];
const month = months [ date . getMonth ()];
const day = date . getDate (). toString (). padStart ( 2 , " " );
const now = new Date ();
const sixMonthsAgo = new Date ( now . getTime () - 180 * 24 * 60 * 60 * 1000 );
if ( date > sixMonthsAgo ) {
const hours = date . getHours (). toString (). padStart ( 2 , "0" );
const minutes = date . getMinutes (). toString (). padStart ( 2 , "0" );
return ` ${ month } ${ day } ${ hours } : ${ minutes } ` ;
}
return ` ${ month } ${ day } ${ date . getFullYear () } ` ;
}
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 ;
2026-01-24 19:49:42 -06:00
sudoPassword? : string ;
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 ;
2026-01-24 19:49:42 -06:00
isWarpgate? : boolean ;
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
2026-01-24 19:49:42 -06:00
function execWithSudo (
client : SSHClient ,
command : string ,
sudoPassword : string ,
) : Promise < { stdout : string ; stderr : string ; code : number } > {
return new Promise (( resolve ) => {
const escapedPassword = sudoPassword . replace ( /'/g , "'\"'\"'" );
const sudoCommand = `echo ' ${ escapedPassword } ' | sudo -S ${ command } 2>&1` ;
client . exec ( sudoCommand , ( err , stream ) => {
if ( err ) {
resolve ({ stdout : "" , stderr : err.message , code : 1 });
return ;
}
let stdout = "" ;
let stderr = "" ;
stream . on ( "data" , ( chunk : Buffer ) => {
stdout += chunk . toString ();
});
stream . stderr . on ( "data" , ( chunk : Buffer ) => {
stderr += chunk . toString ();
});
stream . on ( "close" , ( code : number ) => {
stdout = stdout . replace ( /\[sudo\] password for .+?:\s*/g , "" );
resolve ({ stdout , stderr , code : code || 0 });
});
stream . on ( "error" , ( streamErr : Error ) => {
resolve ({ stdout , stderr : streamErr.message , code : 1 });
});
});
});
}
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" ;
}
2026-01-02 00:33:10 -06:00
function detectBinary ( buffer : Buffer ) : boolean {
if ( buffer . length === 0 ) return false ;
const sampleSize = Math . min ( buffer . length , 8192 );
let nullBytes = 0 ;
for ( let i = 0 ; i < sampleSize ; i ++ ) {
const byte = buffer [ i ];
if ( byte === 0 ) {
nullBytes ++ ;
}
if ( byte < 32 && byte !== 9 && byte !== 10 && byte !== 13 ) {
if ( ++ nullBytes > 1 ) return true ;
}
}
return nullBytes / sampleSize > 0.01 ;
}
2026-01-24 19:49:42 -06:00
/**
* @openapi
* /ssh/file_manager/ssh/connect:
* post:
* summary: Connect to SSH for file management
* description: Establishes an SSH/SFTP connection for file manager operations. Supports password, key-based, and keyboard-interactive authentication, as well as jump hosts and SOCKS5 proxies.
* tags:
* - File Manager
* requestBody:
* required: true
* content:
* application/json:
* schema:
* type: object
* required:
* - sessionId
* - ip
* - port
* - username
* properties:
* sessionId:
* type: string
* description: Unique session identifier
* hostId:
* type: number
* description: Host ID from database
* ip:
* type: string
* description: SSH server IP address
* port:
* type: number
* description: SSH server port
* username:
* type: string
* description: SSH username
* password:
* type: string
* description: SSH password (for password auth)
* sshKey:
* type: string
* description: SSH private key (for key-based auth)
* keyPassword:
* type: string
* description: Private key passphrase
* authType:
* type: string
* enum: [password, key, none]
* description: Authentication method
* credentialId:
* type: number
* description: Credential ID to use from database
* userProvidedPassword:
* type: string
* description: User-provided password for keyboard-interactive auth
* forceKeyboardInteractive:
* type: boolean
* description: Force keyboard-interactive authentication
* jumpHosts:
* type: array
* description: Jump host configuration
* items:
* type: object
* properties:
* hostId:
* type: number
* useSocks5:
* type: boolean
* description: Use SOCKS5 proxy
* socks5Host:
* type: string
* description: SOCKS5 proxy host
* socks5Port:
* type: number
* description: SOCKS5 proxy port
* socks5Username:
* type: string
* description: SOCKS5 proxy username
* socks5Password:
* type: string
* description: SOCKS5 proxy password
* socks5ProxyChain:
* type: array
* description: Chain of SOCKS5 proxies
* responses:
* 200:
* description: SSH connection established successfully, or requires TOTP/Warpgate authentication.
* content:
* application/json:
* schema:
* oneOf:
* - type: object
* properties:
* status:
* type: string
* example: success
* message:
* type: string
* connectionLogs:
* type: array
* - type: object
* properties:
* requires_totp:
* type: boolean
* sessionId:
* type: string
* prompt:
* type: string
* connectionLogs:
* type: array
* - type: object
* properties:
* requires_warpgate:
* type: boolean
* sessionId:
* type: string
* url:
* type: string
* securityKey:
* type: string
* connectionLogs:
* type: array
* - type: object
* properties:
* status:
* type: string
* example: auth_required
* reason:
* type: string
* connectionLogs:
* type: array
* 400:
* description: Missing required parameters or invalid SSH key format.
* 401:
* description: Authentication required.
* 500:
* description: SSH connection failed.
*/
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 ;
2026-01-24 19:49:42 -06:00
const connectionLogs : Array < Omit < LogEntry , "id" | "timestamp" >> = [];
connectionLogs . push (
createConnectionLog (
"info" ,
"sftp_connecting" ,
`Initiating SFTP connection to ${ username } @ ${ ip } : ${ port } ` ,
),
);
2025-10-01 15:40:10 -05:00
if ( ! userId ) {
fileLogger . error ( "SSH connection rejected: no authenticated user" , {
operation : "file_connect_auth" ,
sessionId ,
});
2026-01-24 19:49:42 -06:00
connectionLogs . push (
createConnectionLog (
"error" ,
"sftp_auth" ,
"Authentication required - no user session" ,
),
);
return res
. status ( 401 )
. json ({ error : "Authentication required" , connectionLogs });
2025-10-01 15:40:10 -05:00
}
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
});
2026-01-24 19:49:42 -06:00
connectionLogs . push (
createConnectionLog (
"error" ,
"sftp_connecting" ,
"Missing required connection parameters" ,
),
);
return res
. status ( 400 )
. json ({ error : "Missing SSH connection parameters" , connectionLogs });
2025-09-12 14:42:00 -05:00
}
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
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
2026-01-24 19:49:42 -06:00
connectionLogs . push (
createConnectionLog (
"info" ,
"sftp_auth" ,
"Resolving authentication credentials" ,
),
);
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
};
2026-01-24 19:49:42 -06:00
connectionLogs . push (
createConnectionLog (
"info" ,
"sftp_auth" ,
"Credentials resolved from credential store" ,
),
);
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 ,
});
2026-01-24 19:49:42 -06:00
connectionLogs . push (
createConnectionLog (
"warning" ,
"sftp_auth" ,
"No stored credentials found, using provided credentials" ,
),
);
2025-09-12 14:42:00 -05:00
}
} 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" ,
});
2026-01-24 19:49:42 -06:00
connectionLogs . push (
createConnectionLog (
"warning" ,
"sftp_auth" ,
`Failed to resolve credentials: ${ 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 ;
2026-01-24 19:49:42 -06:00
connectionLogs . push (
createConnectionLog (
"info" ,
"sftp_auth" ,
"Using SSH key authentication" ,
),
);
2025-09-12 14:42:00 -05:00
} catch ( keyError ) {
fileLogger . error ( "SSH key format error for file manager" , {
operation : "file_connect" ,
sessionId ,
hostId ,
error : keyError.message ,
});
2026-01-24 19:49:42 -06:00
connectionLogs . push (
createConnectionLog (
"error" ,
"sftp_auth" ,
`Invalid SSH key format: ${ keyError . message } ` ,
),
);
return res
. status ( 400 )
. json ({ error : "Invalid SSH key format" , connectionLogs });
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 ()) {
2026-01-24 19:49:42 -06:00
connectionLogs . push (
createConnectionLog (
"error" ,
"sftp_auth" ,
"Password required for password authentication" ,
),
);
return res . status ( 400 ). json ({
error : "Password required for password authentication" ,
connectionLogs ,
});
2025-11-05 10:36:16 -06:00
}
2025-12-31 22:20:12 -06:00
config . password = resolvedCredentials . password ;
2026-01-24 19:49:42 -06:00
connectionLogs . push (
createConnectionLog ( "info" , "sftp_auth" , "Using password authentication" ),
);
2025-11-05 10:36:16 -06:00
} else if ( resolvedCredentials . authType === "none" ) {
2026-01-24 19:49:42 -06:00
connectionLogs . push (
createConnectionLog (
"info" ,
"sftp_auth" ,
"Using keyboard-interactive authentication" ,
),
);
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 ,
},
);
2026-01-24 19:49:42 -06:00
connectionLogs . push (
createConnectionLog (
"error" ,
"sftp_auth" ,
"No valid authentication method provided" ,
),
);
return res . status ( 400 ). json ({
error : "Either password or SSH key must be provided" ,
connectionLogs ,
});
2025-09-12 14:42:00 -05:00
}
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
2026-01-24 19:49:42 -06:00
connectionLogs . push (
createConnectionLog ( "info" , "dns" , `Resolving DNS for ${ ip } ` ),
);
connectionLogs . push (
createConnectionLog ( "info" , "tcp" , `Connecting to ${ ip } : ${ port } ` ),
);
connectionLogs . push (
createConnectionLog ( "info" , "handshake" , "Initiating SSH handshake" ),
);
connectionLogs . push (
createConnectionLog (
"info" ,
"sftp_connecting" ,
"Establishing SSH connection..." ,
),
);
2025-09-12 14:42:00 -05:00
client . on ( "ready" , () => {
if ( responseSent ) return ;
responseSent = true ;
2026-01-24 19:49:42 -06:00
connectionLogs . push (
createConnectionLog (
"success" ,
"connected" ,
"SSH connection established successfully" ,
),
);
connectionLogs . push (
createConnectionLog (
"success" ,
"sftp_connected" ,
"SFTP session established successfully" ,
),
);
2025-09-12 14:42:00 -05:00
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 );
2026-01-24 19:49:42 -06:00
res . json ({
status : "success" ,
message : "SSH connection established" ,
connectionLogs ,
});
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
2026-01-24 19:49:42 -06:00
let errorStage : ConnectionStage = "error" ;
if (
err . message . includes ( "ENOTFOUND" ) ||
err . message . includes ( "getaddrinfo" )
) {
errorStage = "dns" ;
connectionLogs . push (
createConnectionLog (
"error" ,
errorStage ,
`DNS resolution failed: ${ err . message } ` ,
{ errorCode : ( err as any ). code , errorLevel : ( err as any ). level },
),
);
} else if (
err . message . includes ( "ECONNREFUSED" ) ||
err . message . includes ( "ETIMEDOUT" )
) {
errorStage = "tcp" ;
connectionLogs . push (
createConnectionLog (
"error" ,
errorStage ,
`TCP connection failed: ${ err . message } ` ,
{ errorCode : ( err as any ). code , errorLevel : ( err as any ). level },
),
);
} else if (
err . message . includes ( "handshake" ) ||
err . message . includes ( "key exchange" )
) {
errorStage = "handshake" ;
connectionLogs . push (
createConnectionLog (
"error" ,
errorStage ,
`SSH handshake failed: ${ err . message } ` ,
{ errorCode : ( err as any ). code , errorLevel : ( err as any ). level },
),
);
} else if (
err . message . includes ( "authentication" ) ||
err . message . includes ( "Authentication" )
) {
errorStage = "auth" ;
connectionLogs . push (
createConnectionLog (
"error" ,
errorStage ,
`Authentication failed: ${ err . message } ` ,
{ errorCode : ( err as any ). code , errorLevel : ( err as any ). level },
),
);
} else {
connectionLogs . push (
createConnectionLog (
"error" ,
"error" ,
`SSH connection failed: ${ err . message } ` ,
{ errorCode : ( err as any ). code , errorLevel : ( err as any ). level },
),
);
}
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" ,
2026-01-24 19:49:42 -06:00
connectionLogs ,
2025-11-05 10:36:16 -06:00
});
} else {
2026-01-24 19:49:42 -06:00
res
. status ( 500 )
. json ({ status : "error" , message : err.message , connectionLogs });
2025-11-05 10:36:16 -06:00
}
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 );
2026-01-24 19:49:42 -06:00
const warpgatePattern = /warpgate\s+authentication/i ;
const isWarpgate =
warpgatePattern . test ( name ) ||
warpgatePattern . test ( instructions ) ||
promptTexts . some (( p ) => warpgatePattern . test ( p ));
if ( isWarpgate ) {
const fullText = ` ${ name } \ n ${ instructions } \ n ${ promptTexts . join ( "\n" ) } ` ;
const urlMatch = fullText . match ( /https?:\/\/[^\s\n]+/i );
const keyMatch = fullText . match (
/security key[:\s]+([a-z0-9](?:\s+[a-z0-9]){3}|[a-z0-9]{4})/i ,
);
if ( urlMatch ) {
if ( responseSent ) return ;
responseSent = true ;
keyboardInteractiveResponded = true ;
connectionLogs . push (
createConnectionLog (
"info" ,
"sftp_auth" ,
"Warpgate authentication required" ,
{ url : urlMatch [ 0 ] },
),
);
pendingTOTPSessions [ sessionId ] = {
client ,
finish ,
config ,
createdAt : Date.now (),
sessionId ,
hostId ,
ip ,
port ,
username ,
userId ,
prompts ,
totpPromptIndex : - 1 ,
resolvedPassword : resolvedCredentials.password ,
totpAttempts : 0 ,
isWarpgate : true ,
};
res . json ({
requires_warpgate : true ,
sessionId ,
url : urlMatch [ 0 ],
securityKey : keyMatch ? keyMatch [ 1 ] : "N/A" ,
connectionLogs ,
});
return ;
}
}
2025-11-05 10:36:16 -06:00
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 ;
2026-01-24 19:49:42 -06:00
connectionLogs . push (
createConnectionLog (
"info" ,
"sftp_auth" ,
"TOTP verification required" ,
{ prompt : prompts [ totpPromptIndex ]. prompt },
),
);
2025-11-05 10:36:16 -06:00
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 ,
2026-01-24 19:49:42 -06:00
connectionLogs ,
2025-11-05 10:36:16 -06:00
});
} 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 ))
) {
2026-01-24 19:49:42 -06:00
connectionLogs . push (
createConnectionLog ( "info" , "proxy" , "Connecting via SOCKS5 proxy" , {
proxyHost : socks5Host ,
proxyPort : socks5Port || 1080 ,
}),
);
2025-12-31 22:20:12 -06:00
try {
const socks5Socket = await createSocks5Connection ( ip , port , {
useSocks5 ,
socks5Host ,
socks5Port ,
socks5Username ,
socks5Password ,
socks5ProxyChain : socks5ProxyChain as any ,
});
if ( socks5Socket ) {
2026-01-24 19:49:42 -06:00
connectionLogs . push (
createConnectionLog (
"success" ,
"proxy" ,
"SOCKS5 proxy connected successfully" ,
),
);
2025-12-31 22:20:12 -06:00
config . sock = socks5Socket ;
client . connect ( config );
return ;
} else {
fileLogger . error ( "SOCKS5 socket is null for SFTP" , undefined , {
operation : "sftp_socks5_socket_null" ,
sessionId ,
});
2026-01-24 19:49:42 -06:00
connectionLogs . push (
createConnectionLog (
"error" ,
"proxy" ,
"SOCKS5 socket creation returned null" ,
),
);
2025-12-31 22:20:12 -06:00
}
} catch ( socks5Error ) {
fileLogger . error ( "SOCKS5 connection failed" , socks5Error , {
operation : "socks5_connect" ,
sessionId ,
hostId ,
proxyHost : socks5Host ,
proxyPort : socks5Port || 1080 ,
});
2026-01-24 19:49:42 -06:00
connectionLogs . push (
createConnectionLog (
"error" ,
"proxy" ,
`SOCKS5 proxy connection failed: ${ socks5Error instanceof Error ? socks5Error . message : "Unknown error" } ` ,
),
);
2025-12-31 22:20:12 -06:00
return res . status ( 500 ). json ({
error :
"SOCKS5 proxy connection failed: " +
( socks5Error instanceof Error
? socks5Error . message
: "Unknown error" ),
2026-01-24 19:49:42 -06:00
connectionLogs ,
2025-12-31 22:20:12 -06:00
});
}
} else if ( jumpHosts && jumpHosts . length > 0 && userId ) {
2025-11-17 09:46:05 -06:00
try {
2026-01-24 19:49:42 -06:00
connectionLogs . push (
createConnectionLog (
"info" ,
"jump" ,
`Connecting via ${ jumpHosts . length } jump host(s)` ,
),
);
2025-11-17 09:46:05 -06:00
const jumpClient = await createJumpHostChain ( jumpHosts , userId );
if ( ! jumpClient ) {
fileLogger . error ( "Failed to establish jump host chain" , {
operation : "file_jump_chain" ,
sessionId ,
hostId ,
});
2026-01-24 19:49:42 -06:00
connectionLogs . push (
createConnectionLog (
"error" ,
"jump" ,
"Failed to establish jump host chain" ,
),
);
return res . status ( 500 ). json ({
error : "Failed to connect through jump hosts" ,
connectionLogs ,
});
2025-11-17 09:46:05 -06:00
}
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 ,
});
2026-01-24 19:49:42 -06:00
connectionLogs . push (
createConnectionLog (
"error" ,
"jump" ,
`Failed to forward through jump host: ${ err . message } ` ,
),
);
2025-11-17 09:46:05 -06:00
jumpClient . end ();
return res . status ( 500 ). json ({
error : "Failed to forward through jump host: " + err . message ,
2026-01-24 19:49:42 -06:00
connectionLogs ,
2025-11-17 09:46:05 -06:00
});
}
config . sock = stream ;
client . connect ( config );
});
} catch ( error ) {
fileLogger . error ( "Jump host error" , error , {
operation : "file_jump_host" ,
sessionId ,
hostId ,
});
2026-01-24 19:49:42 -06:00
connectionLogs . push (
createConnectionLog (
"error" ,
"jump" ,
`Jump host error: ${ error instanceof Error ? error . message : "Unknown error" } ` ,
),
);
return res . status ( 500 ). json ({
error : "Failed to connect through jump hosts" ,
connectionLogs ,
});
2025-11-17 09:46:05 -06:00
}
} else {
client . connect ( config );
}
2025-09-12 14:42:00 -05:00
});
2026-01-24 19:49:42 -06:00
/**
* @openapi
* /ssh/file_manager/ssh/connect-totp:
* post:
* summary: Verify TOTP and complete connection
* description: Verifies the TOTP code and completes the SSH connection for file manager.
* tags:
* - File Manager
* responses:
* 200:
* description: TOTP verified, SSH connection established.
* 400:
* description: Session ID and TOTP code required.
* 401:
* description: Invalid TOTP code or authentication required.
* 404:
* description: TOTP session expired.
* 408:
* description: TOTP session timeout.
*/
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 );
});
2026-01-24 19:49:42 -06:00
/**
* @openapi
* /ssh/file_manager/ssh/connect-warpgate:
* post:
* summary: Complete Warpgate authentication
* description: Submits empty response to complete Warpgate authentication after user completes browser auth.
* tags:
* - File Manager
* requestBody:
* required: true
* content:
* application/json:
* schema:
* type: object
* required:
* - sessionId
* properties:
* sessionId:
* type: string
* description: Session ID from initial connection attempt
* responses:
* 200:
* description: Warpgate authentication completed successfully.
* 401:
* description: Authentication failed or unauthorized.
* 404:
* description: Warpgate session expired.
* 408:
* description: Warpgate session timeout.
*/
app . post ( "/ssh/file_manager/ssh/connect-warpgate" , async ( req , res ) => {
const { sessionId } = req . body ;
const userId = ( req as AuthenticatedRequest ). userId ;
if ( ! userId ) {
fileLogger . error ( "Warpgate verification rejected: no authenticated user" , {
operation : "file_warpgate_auth" ,
sessionId ,
});
return res . status ( 401 ). json ({ error : "Authentication required" });
}
if ( ! sessionId ) {
return res . status ( 400 ). json ({ error : "Session ID required" });
}
const session = pendingTOTPSessions [ sessionId ];
if ( ! session ) {
fileLogger . warn ( "Warpgate session not found or expired" , {
operation : "file_warpgate_verify" ,
sessionId ,
userId ,
availableSessions : Object.keys ( pendingTOTPSessions ),
});
return res
. status ( 404 )
. json ({ error : "Warpgate session expired. Please reconnect." });
}
if ( ! session . isWarpgate ) {
return res . status ( 400 ). json ({ error : "Session is not a Warpgate session" });
}
if ( Date . now () - session . createdAt > 300000 ) {
delete pendingTOTPSessions [ sessionId ];
try {
session . client . end ();
} catch ( error ) {}
fileLogger . warn ( "Warpgate session timeout before completion" , {
operation : "file_warpgate_verify" ,
sessionId ,
userId ,
age : Date.now () - session . createdAt ,
});
return res
. status ( 408 )
. json ({ error : "Warpgate session timeout. Please reconnect." });
}
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 (),
activeOperations : 0 ,
};
scheduleSessionCleanup ( sessionId );
res . json ({
status : "success" ,
message : "Warpgate 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 } ` ;
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" , {
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 ( "Warpgate verification failed" , {
operation : "file_warpgate_verify" ,
sessionId ,
userId ,
error : err.message ,
});
res
. status ( 401 )
. json ({ status : "error" , message : "Warpgate authentication failed" });
});
responseTimeout = setTimeout (() => {
if ( ! responseSent ) {
responseSent = true ;
delete pendingTOTPSessions [ sessionId ];
fileLogger . warn ( "Warpgate verification timeout" , {
operation : "file_warpgate_verify" ,
sessionId ,
userId ,
});
res . status ( 408 ). json ({ error : "Warpgate verification timeout" });
}
}, 60000 );
session . finish ([ "" ]);
});
/**
* @openapi
* /ssh/file_manager/ssh/disconnect:
* post:
* summary: Disconnect from SSH
* description: Closes an active SSH connection for file manager.
* tags:
* - File Manager
* responses:
* 200:
* description: SSH connection disconnected.
*/
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" });
});
2026-01-24 19:49:42 -06:00
/**
* @openapi
* /ssh/file_manager/sudo-password:
* post:
* summary: Set sudo password for session
* description: Stores sudo password temporarily in session for elevated operations.
* tags:
* - File Manager
* requestBody:
* required: true
* content:
* application/json:
* schema:
* type: object
* properties:
* sessionId:
* type: string
* password:
* type: string
* responses:
* 200:
* description: Sudo password set successfully.
* 400:
* description: Invalid session.
*/
app . post ( "/ssh/file_manager/sudo-password" , ( req , res ) => {
const { sessionId , password } = req . body ;
const session = sshSessions [ sessionId ];
if ( ! session || ! session . isConnected ) {
return res . status ( 400 ). json ({ error : "Invalid or disconnected session" });
}
session . sudoPassword = password ;
session . lastActive = Date . now ();
res . json ({ status : "success" , message : "Sudo password set" });
});
/**
* @openapi
* /ssh/file_manager/ssh/status:
* get:
* summary: Get SSH connection status
* description: Checks the status of an SSH connection for file manager.
* tags:
* - File Manager
* parameters:
* - in: query
* name: sessionId
* required: true
* schema:
* type: string
* responses:
* 200:
* description: SSH connection status.
*/
2025-09-12 14:42:00 -05:00
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 });
});
2026-01-24 19:49:42 -06:00
/**
* @openapi
* /ssh/file_manager/ssh/keepalive:
* post:
* summary: Keep SSH session alive
* description: Keeps an active SSH session for file manager alive.
* tags:
* - File Manager
* requestBody:
* required: true
* content:
* application/json:
* schema:
* type: object
* properties:
* sessionId:
* type: string
* responses:
* 200:
* description: Session keepalive successful.
* 400:
* description: Session ID is required or session not found.
*/
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 ,
});
});
2026-01-24 19:49:42 -06:00
/**
* @openapi
* /ssh/file_manager/ssh/listFiles:
* get:
* summary: List files in a directory
* description: Lists the files and directories in a given path on the remote host.
* tags:
* - File Manager
* parameters:
* - in: query
* name: sessionId
* required: true
* schema:
* type: string
* - in: query
* name: path
* required: true
* schema:
* type: string
* responses:
* 200:
* description: A list of files and directories.
* 400:
* description: Session ID is required or SSH connection not established.
* 500:
* description: Failed to list files.
*/
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
2026-01-24 19:49:42 -06:00
const trySFTP = () => {
try {
sshConn . client . sftp (( err , sftp ) => {
if ( err ) {
fileLogger . warn (
`SFTP failed for listFiles, trying fallback: ${ err . message } ` ,
);
tryFallbackMethod ();
return ;
}
sftp . readdir ( sshPath , ( readdirErr , list ) => {
if ( readdirErr ) {
fileLogger . warn (
`SFTP readdir failed, trying fallback: ${ readdirErr . message } ` ,
);
tryFallbackMethod ();
return ;
}
const symlinks : Array < { index : number ; path : string } > = [];
const files : Array < {
name : string ;
type : string ;
size : number | undefined ;
modified : string ;
permissions : string ;
owner : string ;
group : string ;
linkTarget : string | undefined ;
path : string ;
executable : boolean ;
} > = [];
for ( const entry of list ) {
if ( entry . filename === "." || entry . filename === ".." ) continue ;
const attrs = entry . attrs ;
const permissions = modeToPermissions ( attrs . mode );
const isDirectory = attrs . isDirectory ();
const isLink = attrs . isSymbolicLink ();
const fileEntry = {
name : entry.filename ,
type : isDirectory ? "directory" : isLink ? "link" : "file" ,
size : isDirectory ? undefined : attrs . size ,
modified : formatMtime ( attrs . mtime ),
permissions ,
owner : String ( attrs . uid ),
group : String ( attrs . gid ),
linkTarget : undefined as string | undefined ,
path : ` ${ sshPath . endsWith ( "/" ) ? sshPath : sshPath + "/" }${ entry . filename } ` ,
executable :
! isDirectory && ! isLink
? isExecutableFile ( permissions , entry . filename )
: false ,
};
if ( isLink ) {
symlinks . push ({ index : files.length , path : fileEntry.path });
}
files . push ( fileEntry );
}
if ( symlinks . length === 0 ) {
sshConn . activeOperations -- ;
return res . json ({ files , path : sshPath });
}
let resolved = 0 ;
for ( const link of symlinks ) {
sftp . readlink ( link . path , ( linkErr , target ) => {
resolved ++ ;
if ( ! linkErr && target ) {
files [ link . index ]. linkTarget = target ;
}
if ( resolved === symlinks . length ) {
sshConn . activeOperations -- ;
res . json ({ files , path : sshPath });
}
});
}
});
});
} catch ( sftpErr : unknown ) {
const errMsg =
sftpErr instanceof Error ? sftpErr . message : "Unknown error" ;
fileLogger . warn ( `SFTP connection error, trying fallback: ${ errMsg } ` );
tryFallbackMethod ();
2025-09-12 14:42:00 -05:00
}
2026-01-24 19:49:42 -06:00
};
2025-09-12 14:42:00 -05:00
2026-01-24 19:49:42 -06:00
const tryFallbackMethod = () => {
const escapedPath = sshPath . replace ( /'/g , "'\"'\"'" );
sshConn . client . exec ( `command ls -la ' ${ escapedPath } '` , ( err , stream ) => {
if ( err ) {
sshConn . activeOperations -- ;
fileLogger . error ( "SSH listFiles error:" , err );
return res . status ( 500 ). json ({ error : err.message });
2025-09-12 14:42:00 -05:00
}
2026-01-24 19:49:42 -06:00
let data = "" ;
let errorData = "" ;
2025-09-12 14:42:00 -05:00
2026-01-24 19:49:42 -06:00
stream . on ( "data" , ( chunk : Buffer ) => {
data += chunk . toString ();
});
2025-10-01 15:40:10 -05:00
2026-01-24 19:49:42 -06:00
stream . stderr . on ( "data" , ( chunk : Buffer ) => {
errorData += chunk . toString ();
});
2025-10-01 15:40:10 -05:00
2026-01-24 19:49:42 -06:00
stream . on ( "close" , ( code ) => {
if ( code !== 0 ) {
const isPermissionDenied =
errorData . toLowerCase (). includes ( "permission denied" ) ||
errorData . toLowerCase (). includes ( "access denied" );
if ( isPermissionDenied ) {
if ( sshConn . sudoPassword ) {
fileLogger . info (
`Permission denied for listFiles, retrying with sudo: ${ sshPath } ` ,
);
tryWithSudo ();
return ;
}
sshConn . activeOperations -- ;
fileLogger . warn (
`Permission denied for listFiles, sudo required: ${ sshPath } ` ,
);
return res . status ( 403 ). json ({
error : `Permission denied: Cannot access ${ sshPath } ` ,
needsSudo : true ,
path : sshPath ,
});
2025-10-01 15:40:10 -05:00
}
2026-01-24 19:49:42 -06:00
sshConn . activeOperations -- ;
fileLogger . error (
`SSH listFiles command failed with code ${ code } : ${ errorData . replace ( /\n/g , " " ). trim () } ` ,
);
return res
. status ( 500 )
. json ({ error : `Command failed: ${ errorData } ` });
}
sshConn . activeOperations -- ;
2025-09-12 14:42:00 -05:00
2026-01-24 19:49:42 -06:00
const lines = data . split ( "\n" ). filter (( line ) => line . trim ());
const files = [];
2025-09-12 14:42:00 -05:00
2026-01-24 19:49:42 -06:00
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 ];
const owner = parts [ 2 ];
const group = parts [ 3 ];
const size = parseInt ( parts [ 4 ], 10 );
let dateStr = "" ;
const nameStartIndex = 8 ;
if ( parts [ 5 ] && parts [ 6 ] && parts [ 7 ]) {
dateStr = ` ${ parts [ 5 ] } ${ parts [ 6 ] } ${ parts [ 7 ] } ` ;
}
const name = parts . slice ( nameStartIndex ). join ( " " );
const isDirectory = permissions . startsWith ( "d" );
const isLink = permissions . startsWith ( "l" );
if ( name === "." || name === ".." ) continue ;
let actualName = name ;
let linkTarget = undefined ;
if ( isLink && name . includes ( " -> " )) {
const linkParts = name . split ( " -> " );
actualName = linkParts [ 0 ];
linkTarget = linkParts [ 1 ];
}
files . push ({
name : actualName ,
type : isDirectory ? "directory" : isLink ? "link" : "file" ,
size : isDirectory ? undefined : size ,
modified : dateStr ,
permissions ,
owner ,
group ,
linkTarget ,
path : ` ${ sshPath . endsWith ( "/" ) ? sshPath : sshPath + "/" }${ actualName } ` ,
executable :
! isDirectory && ! isLink
? isExecutableFile ( permissions , actualName )
: false ,
});
2025-10-01 15:40:10 -05:00
}
2026-01-24 19:49:42 -06:00
}
2025-10-01 15:40:10 -05:00
2026-01-24 19:49:42 -06:00
res . json ({ files , path : sshPath });
});
});
};
const tryWithSudo = () => {
const escapedPath = sshPath . replace ( /'/g , "'\"'\"'" );
const escapedPassword = sshConn . sudoPassword ! . replace ( /'/g , "'\"'\"'" );
const sudoCommand = `echo ' ${ escapedPassword } ' | sudo -S ls -la ' ${ escapedPath } ' 2>&1` ;
sshConn . client . exec ( sudoCommand , ( err , stream ) => {
if ( err ) {
sshConn . activeOperations -- ;
fileLogger . error ( "SSH sudo 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 ) => {
sshConn . activeOperations -- ;
data = data . replace ( /\[sudo\] password for .+?:\s*/g , "" );
if (
data . toLowerCase (). includes ( "sorry, try again" ) ||
data . toLowerCase (). includes ( "incorrect password" ) ||
errorData . toLowerCase (). includes ( "sorry, try again" )
) {
sshConn . sudoPassword = undefined ;
return res . status ( 403 ). json ({
error : "Sudo authentication failed. Please try again." ,
needsSudo : true ,
sudoFailed : true ,
path : sshPath ,
2025-09-12 14:42:00 -05:00
});
}
2026-01-24 19:49:42 -06:00
if ( code !== 0 && ! data . trim ()) {
fileLogger . error (
`SSH sudo listFiles failed with code ${ code } : ${ errorData . replace ( /\n/g , " " ). trim () } ` ,
);
return res
. status ( 500 )
. json ({ error : `Sudo command failed: ${ errorData || data } ` });
}
const lines = data . split ( "\n" ). filter (( line ) => line . trim ());
const files : Array < {
name : string ;
type : string ;
size : number | undefined ;
modified : string ;
permissions : string ;
owner : string ;
group : string ;
linkTarget : string | undefined ;
path : string ;
executable : boolean ;
} > = [];
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 ];
const owner = parts [ 2 ];
const group = parts [ 3 ];
const size = parseInt ( parts [ 4 ], 10 );
let dateStr = "" ;
const nameStartIndex = 8 ;
if ( parts [ 5 ] && parts [ 6 ] && parts [ 7 ]) {
dateStr = ` ${ parts [ 5 ] } ${ parts [ 6 ] } ${ parts [ 7 ] } ` ;
}
const name = parts . slice ( nameStartIndex ). join ( " " );
const isDirectory = permissions . startsWith ( "d" );
const isLink = permissions . startsWith ( "l" );
if ( name === "." || name === ".." ) continue ;
let actualName = name ;
let linkTarget = undefined ;
if ( isLink && name . includes ( " -> " )) {
const linkParts = name . split ( " -> " );
actualName = linkParts [ 0 ];
linkTarget = linkParts [ 1 ];
}
files . push ({
name : actualName ,
type : isDirectory ? "directory" : isLink ? "link" : "file" ,
size : isDirectory ? undefined : size ,
modified : dateStr ,
permissions ,
owner ,
group ,
linkTarget ,
path : ` ${ sshPath . endsWith ( "/" ) ? sshPath : sshPath + "/" }${ actualName } ` ,
executable :
! isDirectory && ! isLink
? isExecutableFile ( permissions , actualName )
: false ,
});
}
}
res . json ({ files , path : sshPath });
});
2025-10-01 15:40:10 -05:00
});
2026-01-24 19:49:42 -06:00
};
trySFTP ();
2025-10-01 15:40:10 -05:00
});
2026-01-24 19:49:42 -06:00
/**
* @openapi
* /ssh/file_manager/ssh/identifySymlink:
* get:
* summary: Identify symbolic link
* description: Identifies the target of a symbolic link.
* tags:
* - File Manager
* parameters:
* - in: query
* name: sessionId
* required: true
* schema:
* type: string
* - in: query
* name: path
* required: true
* schema:
* type: string
* responses:
* 200:
* description: Symbolic link information.
* 400:
* description: Missing required parameters or SSH connection not established.
* 500:
* description: Failed to identify symbolic link.
*/
2025-10-01 15:40:10 -05:00
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
});
});
});
2026-01-24 19:49:42 -06:00
/**
* @openapi
* /ssh/file_manager/ssh/readFile:
* get:
* summary: Read a file
* description: Reads the content of a file from the remote host.
* tags:
* - File Manager
* parameters:
* - in: query
* name: sessionId
* required: true
* schema:
* type: string
* - in: query
* name: path
* required: true
* schema:
* type: string
* responses:
* 200:
* description: The content of the file.
* 400:
* description: Missing required parameters or file too large.
* 404:
* description: File not found.
* 500:
* description: Failed to read file.
*/
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 });
}
2026-01-02 00:33:10 -06:00
let binaryData = Buffer . alloc ( 0 );
2025-10-01 15:40:10 -05:00
let errorData = "" ;
stream . on ( "data" , ( chunk : Buffer ) => {
2026-01-02 00:33:10 -06:00
binaryData = Buffer . concat ([ binaryData , chunk ]);
2025-10-01 15:40:10 -05:00
});
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 ,
});
}
2026-01-02 00:33:10 -06:00
const isBinary = detectBinary ( binaryData );
if ( isBinary ) {
const base64Content = binaryData . toString ( "base64" );
res . json ({
content : base64Content ,
path : filePath ,
encoding : "base64" ,
});
} else {
const textContent = binaryData . toString ( "utf8" );
res . json ({
content : textContent ,
path : filePath ,
encoding : "utf8" ,
});
}
2025-10-01 15:40:10 -05:00
});
});
});
},
);
2025-09-12 14:42:00 -05:00
});
2026-01-24 19:49:42 -06:00
/**
* @openapi
* /ssh/file_manager/ssh/writeFile:
* post:
* summary: Write to a file
* description: Writes content to a file on the remote host.
* tags:
* - File Manager
* requestBody:
* required: true
* content:
* application/json:
* schema:
* type: object
* properties:
* sessionId:
* type: string
* path:
* type: string
* content:
* type: string
* responses:
* 200:
* description: File written successfully.
* 400:
* description: Missing required parameters or SSH connection not established.
* 500:
* description: Failed to write file.
*/
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" ) {
2026-01-02 00:33:10 -06:00
try {
const testBuffer = Buffer . from ( content , "base64" );
if ( testBuffer . toString ( "base64" ) === content ) {
fileBuffer = testBuffer ;
} else {
fileBuffer = Buffer . from ( content , "utf8" );
}
} catch {
fileBuffer = Buffer . from ( content , "utf8" );
}
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 } ` ,
2026-01-24 19:49:42 -06:00
toast : {
type : "error" ,
message : `Write failed: ${ err . message } ` ,
},
2025-09-12 14:42:00 -05:00
});
}
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
});
2026-01-24 19:49:42 -06:00
/**
* @openapi
* /ssh/file_manager/ssh/uploadFile:
* post:
* summary: Upload a file
* description: Uploads a file to the remote host.
* tags:
* - File Manager
* requestBody:
* required: true
* content:
* application/json:
* schema:
* type: object
* properties:
* sessionId:
* type: string
* path:
* type: string
* content:
* type: string
* fileName:
* type: string
* responses:
* 200:
* description: File uploaded successfully.
* 400:
* description: Missing required parameters or SSH connection not established.
* 500:
* description: Failed to upload file.
*/
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
});
2026-01-24 19:49:42 -06:00
/**
* @openapi
* /ssh/file_manager/ssh/createFile:
* post:
* summary: Create a file
* description: Creates an empty file on the remote host.
* tags:
* - File Manager
* requestBody:
* required: true
* content:
* application/json:
* schema:
* type: object
* properties:
* sessionId:
* type: string
* path:
* type: string
* fileName:
* type: string
* responses:
* 200:
* description: File created successfully.
* 400:
* description: Missing required parameters or SSH connection not established.
* 403:
* description: Permission denied.
* 500:
* description: Failed to create file.
*/
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
});
2026-01-24 19:49:42 -06:00
/**
* @openapi
* /ssh/file_manager/ssh/createFolder:
* post:
* summary: Create a folder
* description: Creates a new folder on the remote host.
* tags:
* - File Manager
* requestBody:
* required: true
* content:
* application/json:
* schema:
* type: object
* properties:
* sessionId:
* type: string
* path:
* type: string
* folderName:
* type: string
* responses:
* 200:
* description: Folder created successfully.
* 400:
* description: Missing required parameters or SSH connection not established.
* 403:
* description: Permission denied.
* 500:
* description: Failed to create folder.
*/
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
});
2026-01-24 19:49:42 -06:00
/**
* @openapi
* /ssh/file_manager/ssh/deleteItem:
* delete:
* summary: Delete a file or directory
* description: Deletes a file or directory on the remote host.
* tags:
* - File Manager
* requestBody:
* required: true
* content:
* application/json:
* schema:
* type: object
* properties:
* sessionId:
* type: string
* path:
* type: string
* isDirectory:
* type: boolean
* responses:
* 200:
* description: Item deleted successfully.
* 400:
* description: Missing required parameters or SSH connection not established.
* 403:
* description: Permission denied.
* 500:
* description: Failed to delete item.
*/
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
2026-01-24 19:49:42 -06:00
? `rm -rf ' ${ escapedPath } '`
: `rm -f ' ${ escapedPath } '` ;
2025-09-12 14:42:00 -05:00
2026-01-24 19:49:42 -06:00
const executeDelete = ( useSudo : boolean ) : Promise < void > => {
return new Promise (( resolve ) => {
if ( useSudo && sshConn . sudoPassword ) {
execWithSudo ( sshConn . client , deleteCommand , sshConn . sudoPassword ). then (
( result ) => {
if (
result . code === 0 ||
( ! result . stderr . includes ( "Permission denied" ) &&
! result . stdout . includes ( "Permission denied" ))
) {
res . json ({
message : "Item deleted successfully" ,
path : itemPath ,
toast : {
type : "success" ,
message : ` ${ isDirectory ? "Directory" : "File" } deleted: ${ itemPath } ` ,
},
});
} else {
res . status ( 500 ). json ({
error : `Delete failed: ${ result . stderr || result . stdout } ` ,
});
}
resolve ();
2025-09-12 14:42:00 -05:00
},
2026-01-24 19:49:42 -06:00
);
return ;
2025-09-12 14:42:00 -05:00
}
2026-01-24 19:49:42 -06:00
sshConn . client . exec (
` ${ deleteCommand } && echo "SUCCESS"` ,
( err , stream ) => {
if ( err ) {
fileLogger . error ( "SSH deleteItem error:" , err );
res . status ( 500 ). json ({ error : err.message });
resolve ();
return ;
}
let outputData = "" ;
let errorData = "" ;
let permissionDenied = false ;
stream . on ( "data" , ( chunk : Buffer ) => {
outputData += chunk . toString ();
});
stream . stderr . on ( "data" , ( chunk : Buffer ) => {
errorData += chunk . toString ();
if ( chunk . toString (). includes ( "Permission denied" )) {
permissionDenied = true ;
}
});
stream . on ( "close" , ( code ) => {
if ( permissionDenied ) {
if ( sshConn . sudoPassword ) {
executeDelete ( true ). then ( resolve );
return ;
}
fileLogger . error ( `Permission denied deleting: ${ itemPath } ` );
res . status ( 403 ). json ({
error : `Permission denied: Cannot delete ${ itemPath } .` ,
needsSudo : true ,
});
resolve ();
return ;
}
if ( outputData . includes ( "SUCCESS" ) || code === 0 ) {
res . json ({
message : "Item deleted successfully" ,
path : itemPath ,
toast : {
type : "success" ,
message : ` ${ isDirectory ? "Directory" : "File" } deleted: ${ itemPath } ` ,
},
});
} else {
res . status ( 500 ). json ({
error : `Command failed: ${ errorData } ` ,
});
}
resolve ();
});
stream . on ( "error" , ( streamErr ) => {
fileLogger . error ( "SSH deleteItem stream error:" , streamErr );
res
. status ( 500 )
. json ({ error : `Stream error: ${ streamErr . message } ` });
resolve ();
});
},
);
2025-09-12 14:42:00 -05:00
});
2026-01-24 19:49:42 -06:00
};
await executeDelete ( false );
2025-09-12 14:42:00 -05:00
});
2026-01-24 19:49:42 -06:00
/**
* @openapi
* /ssh/file_manager/ssh/renameItem:
* put:
* summary: Rename a file or directory
* description: Renames a file or directory on the remote host.
* tags:
* - File Manager
* requestBody:
* required: true
* content:
* application/json:
* schema:
* type: object
* properties:
* sessionId:
* type: string
* oldPath:
* type: string
* newName:
* type: string
* responses:
* 200:
* description: Item renamed successfully.
* 400:
* description: Missing required parameters or SSH connection not established.
* 403:
* description: Permission denied.
* 500:
* description: Failed to rename item.
*/
2025-09-12 14:42:00 -05:00
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 } ` });
}
});
});
});
2026-01-24 19:49:42 -06:00
/**
* @openapi
* /ssh/file_manager/ssh/moveItem:
* put:
* summary: Move a file or directory
* description: Moves a file or directory on the remote host.
* tags:
* - File Manager
* requestBody:
* required: true
* content:
* application/json:
* schema:
* type: object
* properties:
* sessionId:
* type: string
* oldPath:
* type: string
* newPath:
* type: string
* responses:
* 200:
* description: Item moved successfully.
* 400:
* description: Missing required parameters or SSH connection not established.
* 403:
* description: Permission denied.
* 408:
* description: Move operation timed out.
* 500:
* description: Failed to move item.
*/
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 } ` });
}
});
});
});
2026-01-24 19:49:42 -06:00
/**
* @openapi
* /ssh/file_manager/ssh/downloadFile:
* post:
* summary: Download a file
* description: Downloads a file from the remote host.
* tags:
* - File Manager
* requestBody:
* required: true
* content:
* application/json:
* schema:
* type: object
* properties:
* sessionId:
* type: string
* path:
* type: string
* hostId:
* type: integer
* userId:
* type: string
* responses:
* 200:
* description: The file content.
* 400:
* description: Missing required parameters or file too large.
* 500:
* description: Failed to download file.
*/
2025-10-01 15:40:10 -05:00
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 ,
});
});
});
});
});
2026-01-24 19:49:42 -06:00
/**
* @openapi
* /ssh/file_manager/ssh/copyItem:
* post:
* summary: Copy a file or directory
* description: Copies a file or directory on the remote host.
* tags:
* - File Manager
* requestBody:
* required: true
* content:
* application/json:
* schema:
* type: object
* properties:
* sessionId:
* type: string
* sourcePath:
* type: string
* targetDir:
* type: string
* hostId:
* type: integer
* userId:
* type: string
* responses:
* 200:
* description: Item copied successfully.
* 400:
* description: Missing required parameters or SSH connection not established.
* 500:
* description: Failed to copy item.
*/
2025-10-01 15:40:10 -05:00
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 } ` });
}
});
});
});
});
2026-01-24 19:49:42 -06:00
/**
* @openapi
* /ssh/file_manager/ssh/executeFile:
* post:
* summary: Execute a file
* description: Executes a file on the remote host.
* tags:
* - File Manager
* requestBody:
* required: true
* content:
* application/json:
* schema:
* type: object
* properties:
* sessionId:
* type: string
* filePath:
* type: string
* responses:
* 200:
* description: File execution result.
* 400:
* description: Missing required parameters or SSH connection not available.
* 500:
* description: Failed to execute file.
*/
2025-10-01 15:40:10 -05:00
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" });
}
});
});
});
});
});
2026-01-24 19:49:42 -06:00
/**
* @openapi
* /ssh/file_manager/ssh/changePermissions:
* post:
* summary: Change file permissions
* description: Changes the permissions of a file on the remote host.
* tags:
* - File Manager
* requestBody:
* required: true
* content:
* application/json:
* schema:
* type: object
* properties:
* sessionId:
* type: string
* path:
* type: string
* permissions:
* type: string
* responses:
* 200:
* description: Permissions changed successfully.
* 400:
* description: Missing required parameters or SSH connection not available.
* 408:
* description: Permission change timed out.
* 500:
* description: Failed to change permissions.
*/
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" });
}
});
});
});
2026-01-24 19:49:42 -06:00
/**
* @openapi
* /ssh/file_manager/ssh/extractArchive:
* post:
* summary: Extract archive file
* description: Extracts an archive file (.tar, .tar.gz, .tgz, .zip, .tar.bz2, .tbz2, .tar.xz, .txz) to a specified or default location on the remote host.
* tags:
* - File Manager
* requestBody:
* required: true
* content:
* application/json:
* schema:
* type: object
* required:
* - sessionId
* - archivePath
* properties:
* sessionId:
* type: string
* description: SSH session ID
* archivePath:
* type: string
* description: Path to the archive file on remote host
* extractPath:
* type: string
* description: Optional custom extraction path (defaults to same directory as archive)
* responses:
* 200:
* description: Archive extracted successfully.
* content:
* application/json:
* schema:
* type: object
* properties:
* status:
* type: string
* example: success
* message:
* type: string
* extractPath:
* type: string
* 400:
* description: Missing required parameters, SSH connection not established, or unsupported archive format.
* 403:
* description: Permission denied.
* 500:
* description: Failed to extract archive.
*/
2025-11-17 09:46:05 -06:00
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" });
}
});
});
});
2026-01-24 19:49:42 -06:00
/**
* @openapi
* /ssh/file_manager/ssh/compressFiles:
* post:
* summary: Compress files
* description: Compresses files and/or directories on the remote host.
* tags:
* - File Manager
* requestBody:
* required: true
* content:
* application/json:
* schema:
* type: object
* properties:
* sessionId:
* type: string
* paths:
* type: array
* items:
* type: string
* archiveName:
* type: string
* format:
* type: string
* responses:
* 200:
* description: Files compressed successfully.
* 400:
* description: Missing required parameters or unsupported compression format.
* 500:
* description: Failed to compress files.
*/
2025-11-17 09:46:05 -06:00
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
}