diff --git a/src/classes/SshConnectionHandler.ts b/src/classes/SshConnectionHandler.ts index 3e63ce8d..28af8fb9 100644 --- a/src/classes/SshConnectionHandler.ts +++ b/src/classes/SshConnectionHandler.ts @@ -105,9 +105,13 @@ export class SshConnectionHandler { ): void => { logger.verbose('SSH request for a new session'); const session = accept(); - const sessionHandler = new SshSessionHandler(this.authSession?.authToken ?? ''); + const sessionHandler = new SshSessionHandler( + session, + this.authSession?.authToken ?? '', + ); session.on('sftp', sessionHandler.onSftp); session.on('close', sessionHandler.onClose); + session.on('eof', sessionHandler.onEof); }; /** diff --git a/src/classes/SshSessionHandler.ts b/src/classes/SshSessionHandler.ts index 259b3147..9f3d09bb 100644 --- a/src/classes/SshSessionHandler.ts +++ b/src/classes/SshSessionHandler.ts @@ -1,11 +1,20 @@ import { logger } from '../logger'; import { SftpSessionHandler } from './SftpSessionHandler'; -import type { SFTPWrapper } from 'ssh2'; +import type { + Session, + SFTPWrapper, +} from 'ssh2'; export class SshSessionHandler { private readonly authToken: string; - public constructor(authToken: string) { + private readonly session: Session; + + public constructor( + session: Session, + authToken: string, + ) { + this.session = session; this.authToken = authToken; } @@ -50,4 +59,20 @@ export class SshSessionHandler { public onClose = (): void => { logger.verbose('SSH session closed'); }; + + public onEof = (): void => { + // This addresses a bug in the ssh2 library where EOF is not properly + // handled for sftp connections. + // An upstream PR that would fix the behavior: https://github.com/mscdex/ssh2/pull/1111 + // And some context from our own debugging: https://github.com/PermanentOrg/sftp-service/issues/45 + // + // The solution here is not ideal, as it is accessing an undocumented attribute that + // doesn't exist in TypeScript. As a result I need to disable typescript checks. + // + // Once upstream makes that patch this entire handler should become completely unnecessary + // + // !!BEWARE: THERE BE DRAGONS HERE!! + // @ts-expect-error because `_channel` is private / isn't actually documented. + this.session._channel.end(); // eslint-disable-line max-len, no-underscore-dangle, @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access + }; }