@@ -113,6 +113,82 @@ const kIsVerified = Symbol('verified'); |
113 | 113 |
|
114 | 114 | const noop = () => {}; |
115 | 115 |
|
| 116 | +const kTLSSessionStatePrefix = Buffer.from('\0nodejs:tls:session:1\0'); |
| 117 | + |
| 118 | +function getSessionServerIdentity(options) { |
| 119 | + return options?.servername || |
| 120 | + options?.host || |
| 121 | + options?.socket?._host || |
| 122 | + 'localhost'; |
| 123 | +} |
| 124 | + |
| 125 | +function wrapSessionState(session, options) { |
| 126 | + if (!Buffer.isBuffer(session) || options?.isServer) |
| 127 | + return session; |
| 128 | + |
| 129 | + const servername = Buffer.from(getSessionServerIdentity(options), 'utf8'); |
| 130 | + const servernameLength = Buffer.allocUnsafe(2); |
| 131 | + servernameLength.writeUInt16BE(servername.length, 0); |
| 132 | + |
| 133 | + return Buffer.concat([ |
| 134 | + kTLSSessionStatePrefix, |
| 135 | + servernameLength, |
| 136 | + servername, |
| 137 | + session, |
| 138 | + ]); |
| 139 | +} |
| 140 | + |
| 141 | +function unwrapSessionState(session) { |
| 142 | + if (!Buffer.isBuffer(session) || |
| 143 | + session.length < kTLSSessionStatePrefix.length + 2 || |
| 144 | + Buffer.compare( |
| 145 | + session.subarray(0, kTLSSessionStatePrefix.length), |
| 146 | + kTLSSessionStatePrefix, |
| 147 | + ) !== 0) { |
| 148 | + return; |
| 149 | + } |
| 150 | + |
| 151 | + const start = kTLSSessionStatePrefix.length; |
| 152 | + const servernameLength = session.readUInt16BE(start); |
| 153 | + const servernameStart = start + 2; |
| 154 | + const servernameEnd = servernameStart + servernameLength; |
| 155 | + if (session.length < servernameEnd) |
| 156 | + return; |
| 157 | + |
| 158 | + return { |
| 159 | + servername: session.toString('utf8', servernameStart, servernameEnd), |
| 160 | + session: session.subarray(servernameEnd), |
| 161 | + }; |
| 162 | +} |
| 163 | + |
| 164 | +function getSessionForReuse(session, options) { |
| 165 | + if (typeof session === 'string') |
| 166 | + session = Buffer.from(session, 'latin1'); |
| 167 | + |
| 168 | + if (options?.isServer) |
| 169 | + return session; |
| 170 | + |
| 171 | + const wrappedSession = unwrapSessionState(session); |
| 172 | + if (wrappedSession !== undefined) { |
| 173 | + const servername = getSessionServerIdentity(options); |
| 174 | + if (wrappedSession.servername !== servername) { |
| 175 | + debug('ignore session for %s: authenticated for %s', |
| 176 | + servername, wrappedSession.servername); |
| 177 | + return; |
| 178 | + } |
| 179 | + |
| 180 | + return wrappedSession.session; |
| 181 | + } |
| 182 | + |
| 183 | + if (Buffer.isBuffer(session) && options?.rejectUnauthorized !== false) { |
| 184 | + debug('ignore raw session for verified client connection to %s', |
| 185 | + getSessionServerIdentity(options)); |
| 186 | + return; |
| 187 | + } |
| 188 | + |
| 189 | + return session; |
| 190 | +} |
| 191 | + |
116 | 192 | let ipServernameWarned = false; |
117 | 193 | let tlsTracingWarned = false; |
118 | 194 |
|
@@ -341,10 +417,11 @@ function requestOCSPDone(socket) { |
341 | 417 | function onnewsessionclient(sessionId, session) { |
342 | 418 | debug('client emit session'); |
343 | 419 | const owner = this[owner_symbol]; |
| 420 | + const wrappedSession = wrapSessionState(session, owner[kConnectOptions]); |
344 | 421 | if (owner[kIsVerified]) { |
345 | | - owner.emit('session', session); |
| 422 | + owner.emit('session', wrappedSession); |
346 | 423 | } else { |
347 | | - owner[kPendingSession] = session; |
| 424 | + owner[kPendingSession] = wrappedSession; |
348 | 425 | } |
349 | 426 | } |
350 | 427 |
|
@@ -1134,9 +1211,19 @@ TLSSocket.prototype.setServername = function(name) { |
1134 | 1211 | }; |
1135 | 1212 |
|
1136 | 1213 | TLSSocket.prototype.setSession = function(session) { |
1137 | | - if (typeof session === 'string') |
1138 | | - session = Buffer.from(session, 'latin1'); |
1139 | | - this._handle.setSession(session); |
| 1214 | + session = getSessionForReuse(session, this[kConnectOptions] || this._tlsOptions); |
| 1215 | + if (session !== undefined) |
| 1216 | + this._handle.setSession(session); |
| 1217 | +}; |
| 1218 | + |
| 1219 | +TLSSocket.prototype.getSession = function() { |
| 1220 | + if (!this._handle) |
| 1221 | + return null; |
| 1222 | + |
| 1223 | + return wrapSessionState( |
| 1224 | + this._handle.getSession(), |
| 1225 | + this[kConnectOptions] || this._tlsOptions, |
| 1226 | + ); |
1140 | 1227 | }; |
1141 | 1228 |
|
1142 | 1229 | TLSSocket.prototype.getPeerCertificate = function(detailed) { |
@@ -1195,7 +1282,6 @@ function makeSocketMethodProxy(name) { |
1195 | 1282 | 'getFinished', |
1196 | 1283 | 'getPeerFinished', |
1197 | 1284 | 'getProtocol', |
1198 | | - 'getSession', |
1199 | 1285 | 'getTLSTicket', |
1200 | 1286 | 'isSessionReused', |
1201 | 1287 | 'enableTrace', |
@@ -1658,12 +1744,11 @@ function onConnectSecure() { |
1658 | 1744 | // Verify that server's identity matches it's certificate's names |
1659 | 1745 | // Unless server has resumed our existing session |
1660 | 1746 | if (!verifyError && !this.isSessionReused()) { |
1661 | | - const hostname = options.servername || |
1662 | | - options.host || |
1663 | | - (options.socket?._host) || |
1664 | | - 'localhost'; |
1665 | 1747 | const cert = this.getPeerCertificate(true); |
1666 | | - verifyError = options.checkServerIdentity(hostname, cert); |
| 1748 | + verifyError = options.checkServerIdentity( |
| 1749 | + getSessionServerIdentity(options), |
| 1750 | + cert, |
| 1751 | + ); |
1667 | 1752 | } |
1668 | 1753 |
|
1669 | 1754 | if (verifyError) { |
@@ -1740,6 +1825,8 @@ exports.connect = function connect(...args) { |
1740 | 1825 |
|
1741 | 1826 | const context = options.secureContext || tls.createSecureContext(options); |
1742 | 1827 |
|
| 1828 | + options.session = getSessionForReuse(options.session, options); |
| 1829 | + |
1743 | 1830 | const tlssock = new TLSSocket(options.socket, { |
1744 | 1831 | allowHalfOpen: options.allowHalfOpen, |
1745 | 1832 | pipe: !!options.path, |
|