From 9bbdd12ab7855f561ca21fe51dcba367a674a56e Mon Sep 17 00:00:00 2001 From: Artem Zakharchenko Date: Wed, 17 Apr 2024 19:38:11 +0200 Subject: [PATCH] fix(MockHttpSocket): forward tls socket properties --- .../ClientRequest/MockHttpSocket.ts | 37 ++++++++++- .../http/compliance/http-ssl-socket.test.ts | 66 +++++++++++++++++++ 2 files changed, 102 insertions(+), 1 deletion(-) create mode 100644 test/modules/http/compliance/http-ssl-socket.test.ts diff --git a/src/interceptors/ClientRequest/MockHttpSocket.ts b/src/interceptors/ClientRequest/MockHttpSocket.ts index bdc13586..8bbb0716 100644 --- a/src/interceptors/ClientRequest/MockHttpSocket.ts +++ b/src/interceptors/ClientRequest/MockHttpSocket.ts @@ -169,6 +169,28 @@ export class MockHttpSocket extends MockSocket { } } + // Forward TLS Socket properties onto this Socket instance + // in the case of a TLS/SSL connection. + if (Reflect.get(socket, 'encrypted')) { + const tlsProperties = [ + 'encrypted', + 'authorized', + 'getProtocol', + 'getSession', + 'isSessionReused', + ] + + tlsProperties.forEach((propertyName) => { + Object.defineProperty(this, propertyName, { + enumerable: true, + get: () => { + const value = Reflect.get(socket, propertyName) + return typeof value === 'function' ? value.bind(socket) : value + }, + }) + }) + } + socket .on('lookup', (...args) => this.emit('lookup', ...args)) .on('connect', () => { @@ -331,9 +353,22 @@ export class MockHttpSocket extends MockSocket { if (this.baseUrl.protocol === 'https:') { this.emit('secure') this.emit('secureConnect') + // A single TLS connection is represented by two "session" events. - this.emit('session', Buffer.from('mock-session-renegotiate')) + this.emit( + 'session', + this.connectionOptions.session || + Buffer.from('mock-session-renegotiate') + ) this.emit('session', Buffer.from('mock-session-resume')) + + Reflect.set(this, 'encrypted', true) + // The server certificate is not the same as a CA + // passed to the TLS socket connection options. + Reflect.set(this, 'authorized', false) + Reflect.set(this, 'getProtocol', () => 'TLSv1.3') + Reflect.set(this, 'getSession', () => undefined) + Reflect.set(this, 'isSessionReused', () => false) } } diff --git a/test/modules/http/compliance/http-ssl-socket.test.ts b/test/modules/http/compliance/http-ssl-socket.test.ts new file mode 100644 index 00000000..adf096a6 --- /dev/null +++ b/test/modules/http/compliance/http-ssl-socket.test.ts @@ -0,0 +1,66 @@ +/** + * @vitest-environment node + */ +import { it, expect, beforeAll, afterEach, afterAll } from 'vitest' +import https from 'node:https' +import type { TLSSocket } from 'node:tls' +import { DeferredPromise } from '@open-draft/deferred-promise' +import { ClientRequestInterceptor } from '../../../../src/interceptors/ClientRequest' + +const interceptor = new ClientRequestInterceptor() + +beforeAll(() => { + interceptor.apply() +}) + +afterEach(() => { + interceptor.removeAllListeners() +}) + +afterAll(() => { + interceptor.dispose() +}) + +it('emits a correct TLS Socket instance for a handled HTTPS request', async () => { + interceptor.on('request', ({ request }) => { + request.respondWith(new Response('hello world')) + }) + + const request = https.get('https://example.com') + const socketPromise = new DeferredPromise() + request.on('socket', (socket) => { + socket.on('connect', () => socketPromise.resolve(socket as TLSSocket)) + }) + + const socket = await socketPromise + + // Must be a TLS socket. + expect(socket.encrypted).toBe(true) + // The server certificate wasn't signed by one of the CA + // specified in the Socket constructor. + expect(socket.authorized).toBe(false) + + expect(socket.getSession()).toBeUndefined() + expect(socket.getProtocol()).toBe('TLSv1.3') + expect(socket.isSessionReused()).toBe(false) +}) + +it('emits a correct TLS Socket instance for a bypassed HTTPS request', async () => { + const request = https.get('https://example.com') + const socketPromise = new DeferredPromise() + request.on('socket', (socket) => { + socket.on('connect', () => socketPromise.resolve(socket as TLSSocket)) + }) + + const socket = await socketPromise + + // Must be a TLS socket. + expect(socket.encrypted).toBe(true) + // The server certificate wasn't signed by one of the CA + // specified in the Socket constructor. + expect(socket.authorized).toBe(false) + + expect(socket.getSession()).toBeUndefined() + expect(socket.getProtocol()).toBe('TLSv1.3') + expect(socket.isSessionReused()).toBe(false) +})