File
Blob: src/node/internal/internal_tls_wrap.ts
| 1 | // Copyright (c) 2017-2022 Cloudflare, Inc. |
| 2 | // Licensed under the Apache 2.0 license found in the LICENSE file or at: |
| 3 | // https://opensource.org/licenses/Apache-2.0 |
| 4 | // |
| 5 | // Copyright Joyent, Inc. and other Node contributors. |
| 6 | // |
| 7 | // Permission is hereby granted, free of charge, to any person obtaining a |
| 8 | // copy of this software and associated documentation files (the |
| 9 | // "Software"), to deal in the Software without restriction, including |
| 10 | // without limitation the rights to use, copy, modify, merge, publish, |
| 11 | // distribute, sublicense, and/or sell copies of the Software, and to permit |
| 12 | // persons to whom the Software is furnished to do so, subject to the |
| 13 | // following conditions: |
| 14 | // |
| 15 | // The above copyright notice and this permission notice shall be included |
| 16 | // in all copies or substantial portions of the Software. |
| 17 | // |
| 18 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS |
| 19 | // OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF |
| 20 | // MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN |
| 21 | // NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, |
| 22 | // DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR |
| 23 | // OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE |
| 24 | // USE OR OTHER DEALINGS IN THE SOFTWARE. |
| 25 | |
| 26 | import { |
| 27 | Socket, |
| 28 | type SocketOptions, |
| 29 | _normalizeArgs, |
| 30 | onConnectionOpened, |
| 31 | onConnectionClosed, |
| 32 | tryReadStart, |
| 33 | } from 'node-internal:internal_net'; |
| 34 | import { JSStreamSocket } from 'node-internal:internal_tls_jsstream'; |
| 35 | import { checkServerIdentity } from 'node-internal:internal_tls'; |
| 36 | import type { |
| 37 | ConnectionOptions, |
| 38 | TlsOptions, |
| 39 | TLSSocket as TLSSocketType, |
| 40 | } from 'node:tls'; |
| 41 | import type { Duplex } from 'node-internal:streams_duplex'; |
| 42 | import type { OnReadOpts, TcpSocketConnectOpts } from 'node:net'; |
| 43 | import { |
| 44 | validateBuffer, |
| 45 | validateString, |
| 46 | validateObject, |
| 47 | validateNumber, |
| 48 | validateFunction, |
| 49 | validateInt32, |
| 50 | validateUint32, |
| 51 | } from 'node-internal:validators'; |
| 52 | import { |
| 53 | ConnResetException, |
| 54 | ERR_TLS_HANDSHAKE_TIMEOUT, |
| 55 | ERR_OPTION_NOT_IMPLEMENTED, |
| 56 | ERR_TLS_INVALID_CONTEXT, |
| 57 | } from 'node-internal:internal_errors'; |
| 58 | import { SecureContext } from 'node-internal:internal_tls_common'; |
| 59 | import { ok } from 'node-internal:internal_assert'; |
| 60 | |
| 61 | const kConnectOptions = Symbol('connect-options'); |
| 62 | const kErrorEmitted = Symbol('error-emitted'); |
| 63 | const kRes = Symbol('res'); |
| 64 | const kPendingSession = Symbol('pendingSession'); |
| 65 | const kIsVerified = Symbol('verified'); |
| 66 | |
| 67 | // @ts-expect-error TS2323 Cannot redeclare error. |
| 68 | export declare class TLSSocket extends Socket { |
| 69 | _hadError: boolean; |
| 70 | _handle: Socket['_handle']; |
| 71 | _init(): void; |
| 72 | _tlsOptions: TlsOptions & |
| 73 | ConnectionOptions & |
| 74 | SocketOptions & |
| 75 | TcpSocketConnectOpts & { |
| 76 | isServer?: boolean; |
| 77 | requestOCSP?: boolean; |
| 78 | server?: unknown; |
| 79 | onread?: OnReadOpts; |
| 80 | }; |
| 81 | _secureEstablished: boolean; |
| 82 | _securePending: boolean; |
| 83 | _newSessionPending: boolean; |
| 84 | _controlReleased: boolean; |
| 85 | authorized: boolean; |
| 86 | encrypted: boolean; |
| 87 | handle: ReturnType<TLSSocket['_wrapHandle']>; |
| 88 | servername: null | string; |
| 89 | secureConnecting: boolean; |
| 90 | ssl: TLSSocket['_handle']; |
| 91 | [kRes]: null | Socket['_handle']; |
| 92 | [kIsVerified]: boolean; |
| 93 | [kPendingSession]: null | Buffer; |
| 94 | [kErrorEmitted]: boolean; |
| 95 | [kConnectOptions]?: NormalizedConnectionOptions; |
| 96 | |
| 97 | constructor( |
| 98 | socket: Socket | Duplex | undefined, |
| 99 | opts: TLSSocket['_tlsOptions'] |
| 100 | ); |
| 101 | prototype: TLSSocket; |
| 102 | |
| 103 | _destroySSL(): void; |
| 104 | _emitTLSError(error: Error): void; |
| 105 | _finishInit(): void; |
| 106 | _handleTimeout(): void; |
| 107 | _releaseControl(): boolean; |
| 108 | _start(): void; |
| 109 | _tlsError(error: Error): Error | null; |
| 110 | _wrapHandle( |
| 111 | wrap: null | Socket, |
| 112 | handle: Socket['_handle'] | null | undefined, |
| 113 | wrapHasActiveWriteFromPrevOwner: boolean |
| 114 | ): unknown; |
| 115 | disableRenegotiation(): void; |
| 116 | getX509Certificate(): ReturnType<TLSSocketType['getX509Certificate']>; |
| 117 | setKeyCert(context: unknown): void; |
| 118 | setServername(name: string): void; |
| 119 | setSession(session: string | Buffer): void; |
| 120 | setMaxSendFragment(size: number): boolean; |
| 121 | getCertificate(): ReturnType<TLSSocketType['getCertificate']>; |
| 122 | getPeerX509Certificate(): ReturnType<TLSSocketType['getPeerX509Certificate']>; |
| 123 | renegotiate( |
| 124 | options: { |
| 125 | rejectUnauthorized?: boolean | undefined; |
| 126 | requestCert?: boolean | undefined; |
| 127 | }, |
| 128 | callback?: (error: Error | null) => void |
| 129 | ): boolean; |
| 130 | exportKeyingMaterial(length: number, label: string, context?: Buffer): Buffer; |
| 131 | } |
| 132 | |
| 133 | function onnewsessionclient( |
| 134 | this: TLSSocket, |
| 135 | _sessionId: string, |
| 136 | session: Buffer |
| 137 | ): void { |
| 138 | if (this[kIsVerified]) { |
| 139 | this.emit('session', session); |
| 140 | } else { |
| 141 | this[kPendingSession] = session; |
| 142 | } |
| 143 | } |
| 144 | |
| 145 | function onerror(this: TLSSocket, err: Error): void { |
| 146 | if (this._hadError) return; |
| 147 | |
| 148 | this._hadError = true; |
| 149 | |
| 150 | // Destroy socket if error happened before handshake's finish |
| 151 | if (!this._secureEstablished) { |
| 152 | // When handshake fails control is not yet released, |
| 153 | // so self._tlsError will return null instead of actual error |
| 154 | |
| 155 | // Set closing the socket after emitting an event since the socket needs to |
| 156 | // be accessible when the `tlsClientError` event is emitted. |
| 157 | this.destroy(err); |
| 158 | } else { |
| 159 | // Emit error |
| 160 | this._emitTLSError(err); |
| 161 | } |
| 162 | } |
| 163 | |
| 164 | // We are using old style function classes for node.js compat. |
| 165 | // @ts-expect-error TS2323 Cannot redeclare error. |
| 166 | export function TLSSocket( |
| 167 | this: TLSSocket, |
| 168 | socket: Socket | Duplex | undefined, |
| 169 | opts: TLSSocket['_tlsOptions'] |
| 170 | ): void { |
| 171 | const tlsOptions = { ...opts }; |
| 172 | |
| 173 | if (tlsOptions.enableTrace) { |
| 174 | throw new ERR_OPTION_NOT_IMPLEMENTED('options.enableTrace'); |
| 175 | } |
| 176 | |
| 177 | if (tlsOptions.isServer) { |
| 178 | throw new ERR_OPTION_NOT_IMPLEMENTED('options.isServer'); |
| 179 | } |
| 180 | |
| 181 | if (tlsOptions.server) { |
| 182 | throw new ERR_OPTION_NOT_IMPLEMENTED('options.server'); |
| 183 | } |
| 184 | |
| 185 | if (tlsOptions.requestCert) { |
| 186 | // Servers will request certificate from clients. |
| 187 | // Does not apply to Cloudflare Workers. |
| 188 | throw new ERR_OPTION_NOT_IMPLEMENTED('options.requestCert'); |
| 189 | } |
| 190 | |
| 191 | if (tlsOptions.rejectUnauthorized === false) { |
| 192 | // TODO(soon): We don't support rejectUnauthorized=false |
| 193 | throw new ERR_OPTION_NOT_IMPLEMENTED('options.rejectUnauthorized'); |
| 194 | } |
| 195 | |
| 196 | if (tlsOptions.ALPNProtocols !== undefined) { |
| 197 | // Does not apply to Cloudflare Workers. |
| 198 | throw new ERR_OPTION_NOT_IMPLEMENTED('options.ALPNProtocols'); |
| 199 | } |
| 200 | |
| 201 | if (tlsOptions.SNICallback !== undefined) { |
| 202 | // Does not apply to Cloudflare Workers. |
| 203 | throw new ERR_OPTION_NOT_IMPLEMENTED('options.SNICallback'); |
| 204 | } |
| 205 | |
| 206 | if (tlsOptions.requestOCSP) { |
| 207 | // Not yet supported. Can be implemented in the future. |
| 208 | throw new ERR_OPTION_NOT_IMPLEMENTED('options.requestOCSP'); |
| 209 | } |
| 210 | |
| 211 | if ( |
| 212 | tlsOptions.secureContext !== undefined && |
| 213 | !(tlsOptions.secureContext instanceof SecureContext) |
| 214 | ) { |
| 215 | throw new ERR_TLS_INVALID_CONTEXT('context'); |
| 216 | } |
| 217 | |
| 218 | if (tlsOptions.pskCallback !== undefined) { |
| 219 | // Used for TLS-PSK negotiation. We do not support it. |
| 220 | throw new ERR_OPTION_NOT_IMPLEMENTED('options.pskCallback'); |
| 221 | } |
| 222 | |
| 223 | // TODO(soon): Call this on secureConnect once connect() api supports |
| 224 | // getting peer certificate. |
| 225 | if (tlsOptions.checkServerIdentity !== undefined) { |
| 226 | validateFunction( |
| 227 | tlsOptions.checkServerIdentity, |
| 228 | 'options.checkServerIdentity' |
| 229 | ); |
| 230 | } |
| 231 | |
| 232 | this._tlsOptions = tlsOptions; |
| 233 | this._secureEstablished = false; |
| 234 | this._securePending = false; |
| 235 | this._newSessionPending = false; |
| 236 | this._controlReleased = false; |
| 237 | this.secureConnecting = true; |
| 238 | this.servername = null; |
| 239 | this.authorized = false; |
| 240 | this[kRes] = null; |
| 241 | this[kIsVerified] = false; |
| 242 | this[kPendingSession] = null; |
| 243 | this[kErrorEmitted] = false; |
| 244 | |
| 245 | let wrap: Socket | null = null; |
| 246 | let handle: Socket['_handle'] | null = null; |
| 247 | let wrapHasActiveWriteFromPrevOwner = false; |
| 248 | |
| 249 | if (socket) { |
| 250 | if (socket instanceof Socket) { |
| 251 | wrap = socket; |
| 252 | } else { |
| 253 | wrap = new JSStreamSocket(socket); |
| 254 | } |
| 255 | |
| 256 | handle = wrap._handle; |
| 257 | wrapHasActiveWriteFromPrevOwner = wrap.writableLength > 0; |
| 258 | } |
| 259 | |
| 260 | // Just a documented property to make secure sockets |
| 261 | // distinguishable from regular ones. |
| 262 | this.encrypted = true; |
| 263 | |
| 264 | Reflect.apply(Socket, this, [ |
| 265 | { |
| 266 | handle: this._wrapHandle(wrap, handle, wrapHasActiveWriteFromPrevOwner), |
| 267 | allowHalfOpen: socket ? socket.allowHalfOpen : tlsOptions.allowHalfOpen, |
| 268 | pauseOnCreate: tlsOptions.pauseOnConnect, |
| 269 | manualStart: true, |
| 270 | highWaterMark: tlsOptions.highWaterMark, |
| 271 | onread: !socket ? tlsOptions.onread : null, |
| 272 | signal: tlsOptions.signal, |
| 273 | lookup: tlsOptions.lookup, |
| 274 | } as SocketOptions, |
| 275 | ]); |
| 276 | |
| 277 | this._parent = handle; |
| 278 | this._parentWrap = wrap; |
| 279 | |
| 280 | // Proxy for API compatibility |
| 281 | this.ssl = this._handle; // C++ TLSWrap object |
| 282 | |
| 283 | this.on('error', this._tlsError.bind(this)); |
| 284 | |
| 285 | this._init(); |
| 286 | } |
| 287 | Object.setPrototypeOf(TLSSocket.prototype, Socket.prototype); |
| 288 | Object.setPrototypeOf(TLSSocket, Socket); |
| 289 | |
| 290 | TLSSocket.prototype.disableRenegotiation = function disableRenegotiation( |
| 291 | this: TLSSocket |
| 292 | ): void { |
| 293 | // Do nothing. |
| 294 | }; |
| 295 | |
| 296 | TLSSocket.prototype._wrapHandle = function _wrapHandle( |
| 297 | this: TLSSocket, |
| 298 | wrap: null | Socket, |
| 299 | handle: Socket['_handle'] | null | undefined, |
| 300 | _wrapHasActiveWriteFromPrevOwner: boolean |
| 301 | ): unknown { |
| 302 | if (!handle) { |
| 303 | return null; |
| 304 | } |
| 305 | this[kRes] = handle; |
| 306 | |
| 307 | // Guard against adding multiple listeners, as this method may be called |
| 308 | // repeatedly on the same socket by reinitializeHandle |
| 309 | if (this.listenerCount('close', onSocketCloseDestroySSL) === 0) { |
| 310 | this.once('close', onSocketCloseDestroySSL); |
| 311 | } |
| 312 | |
| 313 | wrap?.once('close', () => this.destroy()); |
| 314 | |
| 315 | return handle; |
| 316 | }; |
| 317 | |
| 318 | function onSocketCloseDestroySSL(this: TLSSocket): void { |
| 319 | // We call destroySSL directly because it is already an async process. |
| 320 | this._destroySSL(); |
| 321 | this[kRes] = null; |
| 322 | } |
| 323 | |
| 324 | TLSSocket.prototype._destroySSL = function _destroySSL(this: TLSSocket): void { |
| 325 | // We disable floating promises rule because we don't want to change the |
| 326 | // function signature and return Promise<void> |
| 327 | // |
| 328 | // eslint-disable-next-line @typescript-eslint/no-floating-promises |
| 329 | this._handle?.socket.close().then(() => { |
| 330 | this[kPendingSession] = null; |
| 331 | this[kIsVerified] = false; |
| 332 | }); |
| 333 | }; |
| 334 | |
| 335 | TLSSocket.prototype._init = function _init(this: TLSSocket): void { |
| 336 | const options = this._tlsOptions; |
| 337 | |
| 338 | this.on('error', onerror.bind(this)); |
| 339 | |
| 340 | // This is emitted from net.Socket class. |
| 341 | this.on('connectionAttempt', onnewsessionclient.bind(this)); |
| 342 | |
| 343 | if (options.handshakeTimeout && options.handshakeTimeout > 0) |
| 344 | this.setTimeout(options.handshakeTimeout, this._handleTimeout.bind(this)); |
| 345 | |
| 346 | // TLSSocket can be initialized with 2 different handles. |
| 347 | // |
| 348 | // 1. Socket instance created by "node:net". In this scenario, we need |
| 349 | // to wait for 'connect' event to be emitted in order to trigger _finishInit(). |
| 350 | // 2. Duplex stream. Duplex streams are initialized through JSStreamSocket class. |
| 351 | // If that's the scenario, we can trigger _finishInit() immediately. Since, there |
| 352 | // is no async calls required to wait. |
| 353 | if (this._parentWrap != null && this._parentWrap instanceof JSStreamSocket) { |
| 354 | queueMicrotask(() => { |
| 355 | this._finishInit(); |
| 356 | ok(this._parentWrap instanceof JSStreamSocket); |
| 357 | this._parentWrap.readStart(); |
| 358 | tryReadStart(this); |
| 359 | }); |
| 360 | } else { |
| 361 | this.on('connect', () => { |
| 362 | this._finishInit(); |
| 363 | }); |
| 364 | } |
| 365 | }; |
| 366 | |
| 367 | TLSSocket.prototype.renegotiate = function ( |
| 368 | this: TLSSocket, |
| 369 | options: { |
| 370 | rejectUnauthorized?: boolean | undefined; |
| 371 | requestCert?: boolean | undefined; |
| 372 | }, |
| 373 | callback?: (error: Error | null) => void |
| 374 | ): boolean { |
| 375 | validateObject(options, 'options'); |
| 376 | if (callback !== undefined) { |
| 377 | validateFunction(callback, 'callback'); |
| 378 | } |
| 379 | // TLS renegotiation is not supported. |
| 380 | return false; |
| 381 | }; |
| 382 | |
| 383 | TLSSocket.prototype.exportKeyingMaterial = function exportKeyingMaterial( |
| 384 | this: TLSSocket, |
| 385 | length: number, |
| 386 | label: string, |
| 387 | context?: Buffer |
| 388 | ): Buffer { |
| 389 | validateUint32(length, 'length', true); |
| 390 | validateString(label, 'label'); |
| 391 | if (context !== undefined) { |
| 392 | validateBuffer(context, 'context'); |
| 393 | } |
| 394 | |
| 395 | throw new Error('exportKeyingMaterial is not implemented'); |
| 396 | }; |
| 397 | |
| 398 | TLSSocket.prototype.setMaxSendFragment = function setMaxSendFragment( |
| 399 | this: TLSSocket, |
| 400 | size: number |
| 401 | ): boolean { |
| 402 | validateInt32(size, 'size'); |
| 403 | // Setting maximum TLS fragment size is not supported. |
| 404 | return false; |
| 405 | }; |
| 406 | |
| 407 | TLSSocket.prototype._handleTimeout = function _handleTimeout( |
| 408 | this: TLSSocket |
| 409 | ): void { |
| 410 | this._emitTLSError(new ERR_TLS_HANDSHAKE_TIMEOUT()); |
| 411 | }; |
| 412 | |
| 413 | TLSSocket.prototype._emitTLSError = function _emitTLSError( |
| 414 | this: TLSSocket, |
| 415 | err: Error |
| 416 | ): void { |
| 417 | const e = this._tlsError(err); |
| 418 | if (e) this.emit('error', e); |
| 419 | }; |
| 420 | |
| 421 | TLSSocket.prototype._tlsError = function _tlsError( |
| 422 | this: TLSSocket, |
| 423 | err: Error |
| 424 | ): Error | null { |
| 425 | this.emit('_tlsError', err); |
| 426 | if (this._controlReleased) return err; |
| 427 | return null; |
| 428 | }; |
| 429 | |
| 430 | TLSSocket.prototype._releaseControl = function _releaseControl( |
| 431 | this: TLSSocket |
| 432 | ): boolean { |
| 433 | if (this._controlReleased) return false; |
| 434 | this._controlReleased = true; |
| 435 | this.removeListener('error', this._tlsError.bind(this)); |
| 436 | return true; |
| 437 | }; |
| 438 | |
| 439 | // This function is called from net.Socket onConnectionOpened() handler. |
| 440 | TLSSocket.prototype._finishInit = function _finishInit(this: TLSSocket): void { |
| 441 | // Guard against getting onhandshakedone() after .destroy(). |
| 442 | // * 1.2: If destroy() during onocspresponse(), then write of next handshake |
| 443 | // record fails, the handshake done info callbacks does not occur, and the |
| 444 | // socket closes. |
| 445 | // * 1.3: The OCSP response comes in the same record that finishes handshake, |
| 446 | // so even after .destroy(), the handshake done info callback occurs |
| 447 | // immediately after onocspresponse(). Ignore it. |
| 448 | if (!this._handle) return; |
| 449 | |
| 450 | this._secureEstablished = true; |
| 451 | if ( |
| 452 | this._tlsOptions.handshakeTimeout && |
| 453 | this._tlsOptions.handshakeTimeout > 0 |
| 454 | ) { |
| 455 | this.setTimeout(0, this._handleTimeout.bind(this)); |
| 456 | } |
| 457 | |
| 458 | this.emit('secure'); |
| 459 | }; |
| 460 | |
| 461 | TLSSocket.prototype._start = function _start(this: TLSSocket): void { |
| 462 | if (this.connecting) { |
| 463 | this.once('connect', this._start.bind(this)); |
| 464 | return; |
| 465 | } |
| 466 | |
| 467 | // If a user calls tls.connect({ socket }) with a socket that is not initialized |
| 468 | // and the socket is not connected, we need to wait for the socket to connect |
| 469 | // before we can complete the process. |
| 470 | // |
| 471 | // Take a look at the following test for this particular edge case: |
| 472 | // https://github.com/nodejs/node/blob/91d8a524ada001103a2d1c6825ca17b8393c183f/test/parallel/test-tls-on-empty-socket.js |
| 473 | if (this._parentWrap != null && this._parentWrap._handle == null) { |
| 474 | this._parentWrap.once('connect', () => { |
| 475 | // We need to update the Socket handle of this TLSSocket |
| 476 | // since it was created after TLSSocket is initialized. |
| 477 | if (this._parentWrap?._handle != null) { |
| 478 | this._handle = this._parentWrap._handle; |
| 479 | } |
| 480 | this._start(); |
| 481 | }); |
| 482 | return; |
| 483 | } |
| 484 | |
| 485 | // Guard against the following cases: |
| 486 | // - Socket was destroyed before the connection was established |
| 487 | // - TLSSocket can not be upgraded if the secureTransport does not support 'starttls' |
| 488 | if (this._handle?.socket.secureTransport !== 'starttls') { |
| 489 | return; |
| 490 | } |
| 491 | |
| 492 | // We first need to release the lock |
| 493 | this._handle.writer.releaseLock(); |
| 494 | this._handle.reader.releaseLock(); |
| 495 | |
| 496 | try { |
| 497 | const { host, port, addressType } = this._handle.options; |
| 498 | const socket = this._handle.socket.startTls(); |
| 499 | |
| 500 | this._handle = { |
| 501 | socket: socket, |
| 502 | writer: socket.writable.getWriter(), |
| 503 | reader: socket.readable.getReader({ mode: 'byob' }), |
| 504 | bytesRead: 0, |
| 505 | bytesWritten: 0, |
| 506 | reading: true, |
| 507 | options: this._handle.options, |
| 508 | }; |
| 509 | |
| 510 | // This is now an encrypted connection. |
| 511 | // There are cases where in node:net we have to distinguish between |
| 512 | // encrypted and unencrypted connections. |
| 513 | this.encrypted = true; |
| 514 | |
| 515 | this._handle.socket.opened.then( |
| 516 | onConnectionOpened.bind(this), |
| 517 | (err: unknown) => { |
| 518 | this.emit('connectionAttemptFailed', host, port, addressType, err); |
| 519 | this.destroy(err as Error); |
| 520 | } |
| 521 | ); |
| 522 | |
| 523 | this._handle.socket.closed.then( |
| 524 | onConnectionClosed.bind(this), |
| 525 | (error: unknown): void => { |
| 526 | // Do not call this.destroy.bind(this) since user can override it. |
| 527 | this.destroy(error as Error); |
| 528 | } |
| 529 | ); |
| 530 | } catch (error) { |
| 531 | this.destroy(error as Error); |
| 532 | } |
| 533 | }; |
| 534 | |
| 535 | TLSSocket.prototype.setServername = function setServername( |
| 536 | this: TLSSocket, |
| 537 | name: string |
| 538 | ): void { |
| 539 | validateString(name, 'name'); |
| 540 | // Pipefitter currently does not provide us a way on the internal |
| 541 | // system and possibly KJ's TLS implementation doesn't provides a way, |
| 542 | // but it is something we will need sooner than later. |
| 543 | }; |
| 544 | |
| 545 | TLSSocket.prototype.setSession = function (_session: string | Buffer): void { |
| 546 | // Do nothing. We don't support setting session. |
| 547 | }; |
| 548 | |
| 549 | // @ts-expect-error TS2322 Inconsistencies between @types/node |
| 550 | TLSSocket.prototype.getPeerCertificate = function ( |
| 551 | _detailed?: boolean |
| 552 | ): ReturnType<TLSSocketType['getPeerCertificate']> { |
| 553 | // Returns an object representing a peer certificate. |
| 554 | // This function is not supported. |
| 555 | throw new Error('getPeerCertificate is not implemented'); |
| 556 | }; |
| 557 | |
| 558 | TLSSocket.prototype.getCertificate = function ( |
| 559 | this: TLSSocket |
| 560 | ): ReturnType<TLSSocketType['getCertificate']> { |
| 561 | // Returns an object representing the local certificate. |
| 562 | // This function is not supported. |
| 563 | throw new Error('TLSSocket.getCertificate is not implemented'); |
| 564 | }; |
| 565 | |
| 566 | TLSSocket.prototype.getPeerX509Certificate = function ( |
| 567 | this: TLSSocket, |
| 568 | _detailed?: boolean |
| 569 | ): ReturnType<TLSSocketType['getPeerX509Certificate']> { |
| 570 | // Returns the peer certificate as an X509 certificate. |
| 571 | // This function is not supported. |
| 572 | throw new Error('TLSSocket.getPeerX509Certificate is not implemented'); |
| 573 | }; |
| 574 | |
| 575 | TLSSocket.prototype.getX509Certificate = function ( |
| 576 | this: TLSSocket |
| 577 | ): ReturnType<TLSSocketType['getX509Certificate']> { |
| 578 | // Returns the local certificate as an X509 certificate. |
| 579 | // This function is not supported. |
| 580 | throw new Error('TLSSocket.getX509Certificate is not implemented'); |
| 581 | }; |
| 582 | |
| 583 | TLSSocket.prototype.setKeyCert = function ( |
| 584 | this: TLSSocket, |
| 585 | _context: unknown |
| 586 | ): void { |
| 587 | // Changing private key and certificate to be used with TCP connection |
| 588 | // is not supported due to the limitations of connect() api. |
| 589 | throw new Error('TLSSocket.setKeyCert is not implemented'); |
| 590 | }; |
| 591 | |
| 592 | // We have this syntax because of the original Node.js implementation. |
| 593 | // They are not supported by Cloudflare Workers but in Node.js |
| 594 | // they are all properties of this._handle[PROP_NAME] |
| 595 | [ |
| 596 | 'getCipher', |
| 597 | 'getSharedSigalgs', |
| 598 | 'getEphemeralKeyInfo', |
| 599 | 'getFinished', |
| 600 | 'getPeerFinished', |
| 601 | 'getProtocol', |
| 602 | 'getSession', |
| 603 | 'getTLSTicket', |
| 604 | 'isSessionReused', |
| 605 | 'enableTrace', |
| 606 | ].forEach((method) => { |
| 607 | // @ts-expect-error TS7053 Omitting... |
| 608 | TLSSocket.prototype[method] = function (): null { |
| 609 | // None of these functions are supported by connect() api. |
| 610 | return null; |
| 611 | }; |
| 612 | }); |
| 613 | |
| 614 | type NormalizedConnectionOptions = ConnectionOptions & |
| 615 | SocketOptions & |
| 616 | TlsOptions & { |
| 617 | host?: string; |
| 618 | port: number; |
| 619 | }; |
| 620 | function normalizeConnectArgs( |
| 621 | listArgs: unknown[] |
| 622 | ): [NormalizedConnectionOptions] | [NormalizedConnectionOptions, VoidFunction] { |
| 623 | const args = _normalizeArgs(listArgs); |
| 624 | const options = args[0] as NormalizedConnectionOptions; |
| 625 | const cb = args[1] as VoidFunction | undefined; |
| 626 | |
| 627 | // If args[0] was options, then normalize dealt with it. |
| 628 | // If args[0] is port, or args[0], args[1] is host, port, we need to |
| 629 | // find the options and merge them in, normalize's options has only |
| 630 | // the host/port/path args that it knows about, not the tls options. |
| 631 | // This means that options.host overrides a host arg. |
| 632 | if (listArgs[1] !== null && typeof listArgs[1] === 'object') { |
| 633 | Object.assign(options, listArgs[1]); |
| 634 | } else if (listArgs[2] !== null && typeof listArgs[2] === 'object') { |
| 635 | Object.assign(options, listArgs[2]); |
| 636 | } |
| 637 | |
| 638 | return cb ? [options, cb] : [options]; |
| 639 | } |
| 640 | |
| 641 | function onConnectSecure(this: TLSSocket): void { |
| 642 | this.authorized = true; |
| 643 | this.secureConnecting = false; |
| 644 | this.emit('secureConnect'); |
| 645 | |
| 646 | this[kIsVerified] = true; |
| 647 | const session = this[kPendingSession]; |
| 648 | this[kPendingSession] = null; |
| 649 | if (session) this.emit('session', session); |
| 650 | |
| 651 | this.removeListener('end', onConnectEnd); |
| 652 | } |
| 653 | |
| 654 | function onConnectEnd(this: TLSSocket): void { |
| 655 | // NOTE: This logic is shared with _http_client.js |
| 656 | if (!this._hadError) { |
| 657 | const options = this[kConnectOptions]; |
| 658 | this._hadError = true; |
| 659 | const error = new ConnResetException( |
| 660 | 'Client network socket disconnected ' + |
| 661 | 'before secure TLS connection was ' + |
| 662 | 'established' |
| 663 | ); |
| 664 | error.path = options?.path; |
| 665 | error.host = options?.host; |
| 666 | error.port = options?.port; |
| 667 | // @ts-expect-error TS2339 Missing types |
| 668 | // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment |
| 669 | error.localAddress = options?.localAddress; |
| 670 | this.destroy(error); |
| 671 | } |
| 672 | } |
| 673 | |
| 674 | // Arguments: [port,] [host,] [options,] [cb] |
| 675 | export function connect(...args: unknown[]): TLSSocket { |
| 676 | args = normalizeConnectArgs(args); |
| 677 | const options = args[0] as NormalizedConnectionOptions; |
| 678 | const cb = args[1] as VoidFunction | undefined; |
| 679 | |
| 680 | if (options.minDHSize !== undefined) { |
| 681 | // We leave this validation for node.js compat. |
| 682 | validateNumber(options.minDHSize, 'options.minDHSize', 1); |
| 683 | // Not supported. |
| 684 | throw new ERR_OPTION_NOT_IMPLEMENTED('options.minDHSize'); |
| 685 | } |
| 686 | |
| 687 | if (options.rejectUnauthorized === false) { |
| 688 | // TODO(soon): We don't support rejectUnauthorized=false |
| 689 | throw new ERR_OPTION_NOT_IMPLEMENTED('options.rejectUnauthorized'); |
| 690 | } |
| 691 | |
| 692 | if (options.pskCallback !== undefined) { |
| 693 | // Used for TLS-PSK negotiation. |
| 694 | // Does not make sense for Cloudflare Workers. |
| 695 | throw new ERR_OPTION_NOT_IMPLEMENTED('options.pskCallback'); |
| 696 | } |
| 697 | |
| 698 | if (options.checkServerIdentity !== undefined) { |
| 699 | validateFunction( |
| 700 | options.checkServerIdentity, |
| 701 | 'options.checkServerIdentity' |
| 702 | ); |
| 703 | } |
| 704 | |
| 705 | // @ts-expect-error TS2345 Type incompatibility between Node.js Duplex and internal Duplex |
| 706 | const tlssock = new TLSSocket(options.socket, { |
| 707 | allowHalfOpen: options.allowHalfOpen, |
| 708 | pipe: !!options.path, |
| 709 | ALPNProtocols: options.ALPNProtocols, |
| 710 | enableTrace: options.enableTrace, |
| 711 | highWaterMark: options.highWaterMark, |
| 712 | secureContext: options.secureContext, |
| 713 | checkServerIdentity: options.checkServerIdentity ?? checkServerIdentity, |
| 714 | onread: options.onread, |
| 715 | signal: options.signal, |
| 716 | lookup: options.lookup, |
| 717 | rejectUnauthorized: |
| 718 | options.rejectUnauthorized !== undefined |
| 719 | ? Boolean(options.rejectUnauthorized) // eslint-disable-line @typescript-eslint/no-unnecessary-type-conversion |
| 720 | : true, |
| 721 | }); |
| 722 | |
| 723 | tlssock[kConnectOptions] = options; |
| 724 | |
| 725 | if (cb) { |
| 726 | tlssock.once('secureConnect', cb); |
| 727 | } |
| 728 | |
| 729 | if (!options.socket) { |
| 730 | // If user provided the socket, it's their responsibility to manage its |
| 731 | // connectivity. If we created one internally, we connect it. |
| 732 | if (options.timeout) { |
| 733 | tlssock.setTimeout(options.timeout); |
| 734 | } |
| 735 | |
| 736 | tlssock.connect(options, tlssock._start.bind(tlssock)); |
| 737 | } |
| 738 | |
| 739 | tlssock._releaseControl(); |
| 740 | |
| 741 | if (options.session) { |
| 742 | tlssock.setSession(options.session); |
| 743 | } |
| 744 | |
| 745 | if (options.socket) { |
| 746 | tlssock._start(); |
| 747 | } |
| 748 | |
| 749 | // The 'secure' event is emitted by the SecurePair object once a secure connection has been established. |
| 750 | tlssock.on('secure', onConnectSecure); |
| 751 | tlssock.prependListener('end', onConnectEnd); |
| 752 | |
| 753 | return tlssock; |
| 754 | } |