Skip to content

Commit e8d9741

Browse files
Support TLS 1.3 (#277)
* Use session event to support TLS 1.3 * Renew TLS session ticket after each data connection TLS 1.3 mandates single-use session tickets (RFC 8446 §4.6.1). After a data connection resumes using the control connection's ticket, the server issues a new ticket on that data connection. Capture it via the 'session' event and store it in tlsSessionStore so the next data connection presents a fresh ticket rather than the already-spent one. Without this, servers enforcing single-use tickets (e.g. ProFTPD with TLS 1.3) accept only the first data connection and reject all subsequent ones with a TLS negotiation failure.
1 parent 5585c9d commit e8d9741

2 files changed

Lines changed: 11 additions & 1 deletion

File tree

src/FtpContext.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,8 @@ export class FTPContext {
6161
ipFamily: number | undefined = undefined
6262
/** Options for TLS connections. */
6363
tlsOptions: TLSConnectionOptions = {}
64+
/** Most recent TLS session from the control connection, used to resume the session on data connections. */
65+
tlsSessionStore: Buffer | undefined = undefined
6466
/** Current task to be resolved or rejected. */
6567
protected _task: Task | undefined
6668
/** A multiline response might be received as multiple chunks. */
@@ -150,6 +152,7 @@ export class FTPContext {
150152
this.dataSocket = undefined
151153
// This being a reset, reset any other state apart from the socket.
152154
this.tlsOptions = {}
155+
this.tlsSessionStore = undefined
153156
this._partialResponse = ""
154157
if (this._socket) {
155158
const newSocketUpgradesExisting = socket.localPort === this._socket.localPort
@@ -175,6 +178,9 @@ export class FTPContext {
175178
// Control being closed without error by server is treated as an error.
176179
socket.on("close", hadError => { if (!hadError) this.closeWithError(new Error("Server closed connection unexpectedly.")) })
177180
this._setupDefaultErrorHandlers(socket, "control socket")
181+
if (socket instanceof TLSSocket) {
182+
socket.on("session", session => { this.tlsSessionStore = session })
183+
}
178184
}
179185
this._socket = socket
180186
}

src/transfer.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -130,8 +130,12 @@ export function connectForPassiveTransfer(host: string, port: number, ftp: FTPCo
130130
// security: If a completely new session would be negotiated, a hacker
131131
// could guess the port and connect to the new data connection before we do
132132
// by just starting his/her own TLS session.
133-
session: ftp.socket.getSession()
133+
session: ftp.tlsSessionStore ?? ftp.socket.getSession()
134134
}))
135+
// When the server issues a new session ticket after this data connection's
136+
// TLS handshake (TLS 1.3 single-use tickets), capture it so the next data
137+
// connection can present a fresh ticket and resume successfully.
138+
socket.on("session", session => { ftp.tlsSessionStore = session })
135139
// It's the responsibility of the transfer task to wait until the
136140
// TLS socket issued the event 'secureConnect'. We can't do this
137141
// here because some servers will start upgrading after the

0 commit comments

Comments
 (0)