basic-ftp-fixed
v5.1.2
Published
Fork of basic-ftp with fixes for TLS 1.3 upload failures (vsftpd 426 bug) and passive mode listener leaks.
Maintainers
Readme
basic-ftp-fixed
Drop-in replacement for basic-ftp 5.1.0 with critical bug fixes for FTPS over TLS 1.3.
Installation
npm install basic-ftp-fixedUsage is identical to basic-ftp — just change the package name in your require() / import.
Fixes
Fix 1: Passive mode listener leak (transfer.js — connectForPassiveTransfer)
Bug: When FTPS wraps a plain TCP socket in TLS, the local variable socket is reassigned to the new TLS socket. The subsequent removeListener("error"/timeout") calls operate on the TLS wrapper (which never had those listeners), leaving the original plain socket with active listeners and a running setTimeout.
Impact: If the plain socket's idle timeout fires, the handleTimeout closure captures the reassigned socket variable (now the TLS wrapper) and calls socket.destroy(), killing the active TLS data connection and causing transfer failures.
Fix: Save a reference to the original plain socket before TLS wrapping. Remove listeners and clear the timeout on the original socket.
Fix 2: TLS 1.3 session ticket caching (FtpContext.js)
Bug: In TLS 1.3, session tickets are delivered asynchronously via the session event after the handshake completes. getSession() may return stale or missing session data if called before the latest ticket arrives.
Fix: Listen for the session event on the control socket and cache the latest session ticket. Data connections prefer the cached session, falling back to getSession().
Fix 3: Upload 426 error with vsftpd + TLS 1.3 (transfer.js — uploadFrom + connectForPassiveTransfer)
Bug: pipeline(source, dataSocket, callback) automatically calls end() on the TLS data socket immediately after the source stream ends. This sends a TLS close_notify alert. On vsftpd 3.0.5 with TLS 1.3, the close_notify arrives before the server has finished reading all data from its internal SSL buffers, causing vsftpd to report 426 Failure reading network stream.
Impact: FTPS uploads to vsftpd servers running TLS 1.3 fail with 426 errors in the vast majority of attempts. The data is fully transmitted from the client's perspective, but the server rejects it.
Root cause: vsftpd 3.0.5 has a bug in its TLS 1.3 close_notify handling — when the client sends close_notify shortly after the last data record, the server's SSL_read() fails to read remaining data from its internal SSL buffers.
Fix: Replace pipeline with manual source.pipe(dataSocket, { end: false }). After the source stream ends, instead of calling dataSocket.end() (which sends TLS close_notify), we close the underlying TCP socket directly — bypassing the TLS layer entirely:
- During passive connection setup, the original plain TCP socket is saved as
dataSocket._plainSocketbefore TLS wrapping. - When the upload source ends, we wait for the
drainevent (if write buffer has backpressure) to ensure all encrypted data has been handed to the OS kernel. - Then we call
plainSocket.end(), which sends a TCPFINwithout any TLSclose_notify. - The server's
SSL_read()reads all application data normally, then encounters an EOF — which vsftpd handles correctly, returning226 Transfer complete.
This approach is fully event-driven (no setTimeout) and avoids the problematic close_notify entirely. For non-TLS (plain FTP) connections, the normal dataSocket.end() is used as a fallback.
Affected Servers
This fix is primarily for vsftpd 3.0.5 with TLS 1.3 enabled, but the listener leak fix (Fix 1) benefits all FTPS connections.
Compatibility
- API: 100% compatible with
[email protected] - Node.js: >= 10.0.0
- License: MIT (same as original)
Credits
Original library by Patrick Juchli.
