From 260f1a190ff2c5629bc923474f29f8224917eae4 Mon Sep 17 00:00:00 2001 From: Francesco Novy Date: Wed, 7 Aug 2024 17:17:44 +0200 Subject: [PATCH 1/2] fix(utils): Streamline IP capturing on incoming requests --- packages/remix/src/utils/web-fetch.ts | 17 -- packages/utils/src/requestdata.ts | 34 ++- .../src}/vendor/getIpAddress.ts | 82 +++-- packages/utils/test/requestdata.test.ts | 285 ++++++++++++++++++ 4 files changed, 356 insertions(+), 62 deletions(-) rename packages/{remix/src/utils => utils/src}/vendor/getIpAddress.ts (65%) diff --git a/packages/remix/src/utils/web-fetch.ts b/packages/remix/src/utils/web-fetch.ts index 8450a12eb05d..6e188bd9d440 100644 --- a/packages/remix/src/utils/web-fetch.ts +++ b/packages/remix/src/utils/web-fetch.ts @@ -1,4 +1,3 @@ -/* eslint-disable complexity */ // Based on Remix's implementation of Fetch API // https://github.com/remix-run/web-std-io/blob/d2a003fe92096aaf97ab2a618b74875ccaadc280/packages/fetch/ // The MIT License (MIT) @@ -23,10 +22,6 @@ // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. -import { logger } from '@sentry/utils'; - -import { DEBUG_BUILD } from './debug-build'; -import { getClientIPAddress } from './vendor/getIpAddress'; import type { RemixRequest } from './vendor/types'; /* @@ -124,15 +119,6 @@ export const normalizeRemixRequest = (request: RemixRequest): Record { + // eslint-disable-next-line @typescript-eslint/no-dynamic-delete + delete (requestData.headers as Record)[ipHeaderName]; + }); + } + break; } case 'method': { @@ -264,9 +270,12 @@ export function addRequestDataToEvent( }; if (include.request) { - const extractedRequestData = Array.isArray(include.request) - ? extractRequestData(req, { include: include.request }) - : extractRequestData(req); + const includeRequest = Array.isArray(include.request) ? [...include.request] : [...DEFAULT_REQUEST_INCLUDES]; + if (include.ip) { + includeRequest.push('ip'); + } + + const extractedRequestData = extractRequestData(req, { include: includeRequest }); event.request = { ...event.request, @@ -288,8 +297,9 @@ export function addRequestDataToEvent( // client ip: // node, nextjs: req.socket.remoteAddress // express, koa: req.ip + // It may also be sent by proxies as specified in X-Forwarded-For or similar headers if (include.ip) { - const ip = req.ip || (req.socket && req.socket.remoteAddress); + const ip = (req.headers && getClientIPAddress(req.headers)) || req.ip || (req.socket && req.socket.remoteAddress); if (ip) { event.user = { ...event.user, diff --git a/packages/remix/src/utils/vendor/getIpAddress.ts b/packages/utils/src/vendor/getIpAddress.ts similarity index 65% rename from packages/remix/src/utils/vendor/getIpAddress.ts rename to packages/utils/src/vendor/getIpAddress.ts index d63e31779aac..b56cc1404eaf 100644 --- a/packages/remix/src/utils/vendor/getIpAddress.ts +++ b/packages/utils/src/vendor/getIpAddress.ts @@ -23,7 +23,21 @@ // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. -import { isIP } from 'net'; +// The headers to check, in priority order +export const ipHeaderNames = [ + 'X-Client-IP', + 'X-Forwarded-For', + 'Fly-Client-IP', + 'CF-Connecting-IP', + 'Fastly-Client-Ip', + 'True-Client-Ip', + 'X-Real-IP', + 'X-Cluster-Client-IP', + 'X-Forwarded', + 'Forwarded-For', + 'Forwarded', + 'X-Vercel-Forwarded-For', +]; /** * Get the IP address of the client sending a request. @@ -31,50 +45,24 @@ import { isIP } from 'net'; * It receives a Request headers object and use it to get the * IP address from one of the following headers in order. * - * - X-Client-IP - * - X-Forwarded-For - * - Fly-Client-IP - * - CF-Connecting-IP - * - Fastly-Client-Ip - * - True-Client-Ip - * - X-Real-IP - * - X-Cluster-Client-IP - * - X-Forwarded - * - Forwarded-For - * - Forwarded - * * If the IP address is valid, it will be returned. Otherwise, null will be * returned. * * If the header values contains more than one IP address, the first valid one * will be returned. */ -export function getClientIPAddress(headers: Headers): string | null { - // The headers to check, in priority order - const headerNames = [ - 'X-Client-IP', - 'X-Forwarded-For', - 'Fly-Client-IP', - 'CF-Connecting-IP', - 'Fastly-Client-Ip', - 'True-Client-Ip', - 'X-Real-IP', - 'X-Cluster-Client-IP', - 'X-Forwarded', - 'Forwarded-For', - 'Forwarded', - ]; - +export function getClientIPAddress(headers: { [key: string]: string | string[] | undefined }): string | null { // This will end up being Array because of the various possible values a header // can take - const headerValues = headerNames.map((headerName: string) => { - const value = headers.get(headerName); + const headerValues = ipHeaderNames.map((headerName: string) => { + const rawValue = headers[headerName]; + const value = Array.isArray(rawValue) ? rawValue.join(';') : rawValue; if (headerName === 'Forwarded') { return parseForwardedHeader(value); } - return value?.split(',').map((v: string) => v.trim()); + return value && value.split(',').map((v: string) => v.trim()); }); // Flatten the array and filter out any falsy entries @@ -92,7 +80,7 @@ export function getClientIPAddress(headers: Headers): string | null { return ipAddress || null; } -function parseForwardedHeader(value: string | null): string | null { +function parseForwardedHeader(value: string | null | undefined): string | null { if (!value) { return null; } @@ -105,3 +93,31 @@ function parseForwardedHeader(value: string | null): string | null { return null; } + +// +/** + * Custom method instead of importing this from `net` package, as this only exists in node + * Accepts: + * 127.0.0.1 + * 192.168.1.1 + * 192.168.1.255 + * 255.255.255.255 + * 10.1.1.1 + * 0.0.0.0 + * + * Rejects: + * 1.1.1.01 + * 30.168.1.255.1 + * 127.1 + * 192.168.1.256 + * -1.2.3.4 + * 1.1.1.1. + * 3...3 + * 192.168.1.099 + */ +function isIP(str: string): boolean { + const regex = + /^((?:[0-9]|[1-9][0-9]|1[0-9][0-9]|2[0-4][0-9]|25[0-5])[.]){3}(?:[0-9]|[1-9][0-9]|1[0-9][0-9]|2[0-4][0-9]|25[0-5])$/; + + return regex.test(str); +} diff --git a/packages/utils/test/requestdata.test.ts b/packages/utils/test/requestdata.test.ts index 7e44f703c62a..73c42a7aa545 100644 --- a/packages/utils/test/requestdata.test.ts +++ b/packages/utils/test/requestdata.test.ts @@ -107,6 +107,227 @@ describe('addRequestDataToEvent', () => { expect(parsedRequest.user!.ip_address).toEqual('321'); }); + + test.each([ + 'X-Client-IP', + 'X-Forwarded-For', + 'Fly-Client-IP', + 'CF-Connecting-IP', + 'Fastly-Client-Ip', + 'True-Client-Ip', + 'X-Real-IP', + 'X-Cluster-Client-IP', + 'X-Forwarded', + 'Forwarded-For', + 'X-Vercel-Forwarded-For', + ])('can be extracted from %s header', headerName => { + const reqWithIPInHeader = { + ...mockReq, + headers: { + [headerName]: '123.5.6.1', + }, + }; + + const optionsWithIP = { + include: { + ip: true, + }, + }; + + const parsedRequest: Event = addRequestDataToEvent(mockEvent, reqWithIPInHeader, optionsWithIP); + + expect(parsedRequest.user!.ip_address).toEqual('123.5.6.1'); + }); + + it('can be extracted from Forwarded header', () => { + const reqWithIPInHeader = { + ...mockReq, + headers: { + Forwarded: 'by=111;for=123.5.6.1;for=123.5.6.2;', + }, + }; + + const optionsWithIP = { + include: { + ip: true, + }, + }; + + const parsedRequest: Event = addRequestDataToEvent(mockEvent, reqWithIPInHeader, optionsWithIP); + + expect(parsedRequest.user!.ip_address).toEqual('123.5.6.1'); + }); + + test('it ignores invalid IP in header', () => { + const reqWithIPInHeader = { + ...mockReq, + headers: { + 'X-Client-IP': 'invalid', + }, + }; + + const optionsWithIP = { + include: { + ip: true, + }, + }; + + const parsedRequest: Event = addRequestDataToEvent(mockEvent, reqWithIPInHeader, optionsWithIP); + + expect(parsedRequest.user!.ip_address).toEqual(undefined); + }); + + test('IP from header takes presedence over socket', () => { + const reqWithIPInHeader = { + ...mockReq, + headers: { + 'X-Client-IP': '123.5.6.1', + }, + socket: { + remoteAddress: '321', + } as net.Socket, + }; + + const optionsWithIP = { + include: { + ip: true, + }, + }; + + const parsedRequest: Event = addRequestDataToEvent(mockEvent, reqWithIPInHeader, optionsWithIP); + + expect(parsedRequest.user!.ip_address).toEqual('123.5.6.1'); + }); + + test('IP from header takes presedence over req.ip', () => { + const reqWithIPInHeader = { + ...mockReq, + headers: { + 'X-Client-IP': '123.5.6.1', + }, + ip: '123', + }; + + const optionsWithIP = { + include: { + ip: true, + }, + }; + + const parsedRequest: Event = addRequestDataToEvent(mockEvent, reqWithIPInHeader, optionsWithIP); + + expect(parsedRequest.user!.ip_address).toEqual('123.5.6.1'); + }); + + test('does not add IP if ip=false', () => { + const reqWithIPInHeader = { + ...mockReq, + headers: { + 'X-Client-IP': '123.5.6.1', + }, + ip: '123', + }; + + const optionsWithoutIP = { + include: { + ip: false, + }, + }; + + const parsedRequest: Event = addRequestDataToEvent(mockEvent, reqWithIPInHeader, optionsWithoutIP); + + expect(parsedRequest.user!.ip_address).toEqual(undefined); + }); + + test('does not add IP by default', () => { + const reqWithIPInHeader = { + ...mockReq, + headers: { + 'X-Client-IP': '123.5.6.1', + }, + ip: '123', + }; + + const optionsWithoutIP = {}; + + const parsedRequest: Event = addRequestDataToEvent(mockEvent, reqWithIPInHeader, optionsWithoutIP); + + expect(parsedRequest.user!.ip_address).toEqual(undefined); + }); + + test('removes IP headers if `ip` is not set in the options', () => { + const reqWithIPInHeader = { + ...mockReq, + headers: { + otherHeader: 'hello', + 'X-Client-IP': '123', + 'X-Forwarded-For': '123', + 'Fly-Client-IP': '123', + 'CF-Connecting-IP': '123', + 'Fastly-Client-Ip': '123', + 'True-Client-Ip': '123', + 'X-Real-IP': '123', + 'X-Cluster-Client-IP': '123', + 'X-Forwarded': '123', + 'Forwarded-For': '123', + Forwarded: '123', + 'X-Vercel-Forwarded-For': '123', + }, + }; + + const optionsWithoutIP = { + include: {}, + }; + + const parsedRequest: Event = addRequestDataToEvent(mockEvent, reqWithIPInHeader, optionsWithoutIP); + + expect(parsedRequest.request?.headers).toEqual({ otherHeader: 'hello' }); + }); + + test('keeps IP headers if `ip=true`', () => { + const reqWithIPInHeader = { + ...mockReq, + headers: { + otherHeader: 'hello', + 'X-Client-IP': '123', + 'X-Forwarded-For': '123', + 'Fly-Client-IP': '123', + 'CF-Connecting-IP': '123', + 'Fastly-Client-Ip': '123', + 'True-Client-Ip': '123', + 'X-Real-IP': '123', + 'X-Cluster-Client-IP': '123', + 'X-Forwarded': '123', + 'Forwarded-For': '123', + Forwarded: '123', + 'X-Vercel-Forwarded-For': '123', + }, + }; + + const optionsWithoutIP = { + include: { + ip: true, + }, + }; + + const parsedRequest: Event = addRequestDataToEvent(mockEvent, reqWithIPInHeader, optionsWithoutIP); + + expect(parsedRequest.request?.headers).toEqual({ + otherHeader: 'hello', + 'X-Client-IP': '123', + 'X-Forwarded-For': '123', + 'Fly-Client-IP': '123', + 'CF-Connecting-IP': '123', + 'Fastly-Client-Ip': '123', + 'True-Client-Ip': '123', + 'X-Real-IP': '123', + 'X-Cluster-Client-IP': '123', + 'X-Forwarded': '123', + 'Forwarded-For': '123', + Forwarded: '123', + 'X-Vercel-Forwarded-For': '123', + }); + }); }); describe('request properties', () => { @@ -269,6 +490,70 @@ describe('extractRequestData', () => { cookies: { foo: 'bar' }, }); }); + + it('removes IP-related headers from requestdata.headers, if `ip` is not set in the options', () => { + const mockReq = { + headers: { + otherHeader: 'hello', + 'X-Client-IP': '123', + 'X-Forwarded-For': '123', + 'Fly-Client-IP': '123', + 'CF-Connecting-IP': '123', + 'Fastly-Client-Ip': '123', + 'True-Client-Ip': '123', + 'X-Real-IP': '123', + 'X-Cluster-Client-IP': '123', + 'X-Forwarded': '123', + 'Forwarded-For': '123', + Forwarded: '123', + 'X-Vercel-Forwarded-For': '123', + }, + }; + const options = { include: ['headers'] }; + + expect(extractRequestData(mockReq, options)).toStrictEqual({ + headers: { otherHeader: 'hello' }, + }); + }); + + it('keeps IP-related headers from requestdata.headers, if `ip` is enabled in options', () => { + const mockReq = { + headers: { + otherHeader: 'hello', + 'X-Client-IP': '123', + 'X-Forwarded-For': '123', + 'Fly-Client-IP': '123', + 'CF-Connecting-IP': '123', + 'Fastly-Client-Ip': '123', + 'True-Client-Ip': '123', + 'X-Real-IP': '123', + 'X-Cluster-Client-IP': '123', + 'X-Forwarded': '123', + 'Forwarded-For': '123', + Forwarded: '123', + 'X-Vercel-Forwarded-For': '123', + }, + }; + const options = { include: ['headers', 'ip'] }; + + expect(extractRequestData(mockReq, options)).toStrictEqual({ + headers: { + otherHeader: 'hello', + 'X-Client-IP': '123', + 'X-Forwarded-For': '123', + 'Fly-Client-IP': '123', + 'CF-Connecting-IP': '123', + 'Fastly-Client-Ip': '123', + 'True-Client-Ip': '123', + 'X-Real-IP': '123', + 'X-Cluster-Client-IP': '123', + 'X-Forwarded': '123', + 'Forwarded-For': '123', + Forwarded: '123', + 'X-Vercel-Forwarded-For': '123', + }, + }); + }); }); describe('cookies', () => { From 57138859ead72cd7cfcd39bb11f60c0751af3b2f Mon Sep 17 00:00:00 2001 From: Francesco Novy Date: Thu, 8 Aug 2024 10:01:26 +0200 Subject: [PATCH 2/2] fixes & tests --- .../remix/test/utils/getIpAddress.test.ts | 42 ------------------- .../test/utils/normalizeRemixRequest.test.ts | 1 - packages/utils/src/vendor/getIpAddress.ts | 4 +- packages/utils/test/requestdata.test.ts | 31 ++++++++++++++ 4 files changed, 33 insertions(+), 45 deletions(-) delete mode 100644 packages/remix/test/utils/getIpAddress.test.ts diff --git a/packages/remix/test/utils/getIpAddress.test.ts b/packages/remix/test/utils/getIpAddress.test.ts deleted file mode 100644 index 9e05dec9515a..000000000000 --- a/packages/remix/test/utils/getIpAddress.test.ts +++ /dev/null @@ -1,42 +0,0 @@ -import { getClientIPAddress } from '../../src/utils/vendor/getIpAddress'; - -class Headers { - private _headers: Record = {}; - - get(key: string): string | null { - return this._headers[key] ?? null; - } - - set(key: string, value: string): void { - this._headers[key] = value; - } -} - -describe('getClientIPAddress', () => { - it.each([ - [ - '2b01:cb19:8350:ed00:d0dd:fa5b:de31:8be5,2b01:cb19:8350:ed00:d0dd:fa5b:de31:8be5, 141.101.69.35', - '2b01:cb19:8350:ed00:d0dd:fa5b:de31:8be5', - ], - [ - '2b01:cb19:8350:ed00:d0dd:fa5b:de31:8be5, 2b01:cb19:8350:ed00:d0dd:fa5b:de31:8be5, 141.101.69.35', - '2b01:cb19:8350:ed00:d0dd:fa5b:de31:8be5', - ], - [ - '2a01:cb19:8350:ed00:d0dd:INVALID_IP_ADDR:8be5,141.101.69.35,2a01:cb19:8350:ed00:d0dd:fa5b:de31:8be5', - '141.101.69.35', - ], - [ - '2b01:cb19:8350:ed00:d0dd:fa5b:nope:8be5, 2b01:cb19:NOPE:ed00:d0dd:fa5b:de31:8be5, 141.101.69.35 ', - '141.101.69.35', - ], - ['2b01:cb19:8350:ed00:d0 dd:fa5b:de31:8be5, 141.101.69.35', '141.101.69.35'], - ])('should parse the IP from the X-Forwarded-For header %s', (headerValue, expectedIP) => { - const headers = new Headers(); - headers.set('X-Forwarded-For', headerValue); - - const ip = getClientIPAddress(headers as any); - - expect(ip).toEqual(expectedIP); - }); -}); diff --git a/packages/remix/test/utils/normalizeRemixRequest.test.ts b/packages/remix/test/utils/normalizeRemixRequest.test.ts index b627a34e4f12..64de88510014 100644 --- a/packages/remix/test/utils/normalizeRemixRequest.test.ts +++ b/packages/remix/test/utils/normalizeRemixRequest.test.ts @@ -83,7 +83,6 @@ describe('normalizeRemixRequest', () => { hostname: 'example.com', href: 'https://example.com/api/json?id=123', insecureHTTPParser: undefined, - ip: null, method: 'GET', originalUrl: 'https://example.com/api/json?id=123', path: '/api/json?id=123', diff --git a/packages/utils/src/vendor/getIpAddress.ts b/packages/utils/src/vendor/getIpAddress.ts index b56cc1404eaf..8b96fe2146af 100644 --- a/packages/utils/src/vendor/getIpAddress.ts +++ b/packages/utils/src/vendor/getIpAddress.ts @@ -104,6 +104,7 @@ function parseForwardedHeader(value: string | null | undefined): string | null { * 255.255.255.255 * 10.1.1.1 * 0.0.0.0 + * 2b01:cb19:8350:ed00:d0dd:fa5b:de31:8be5 * * Rejects: * 1.1.1.01 @@ -117,7 +118,6 @@ function parseForwardedHeader(value: string | null | undefined): string | null { */ function isIP(str: string): boolean { const regex = - /^((?:[0-9]|[1-9][0-9]|1[0-9][0-9]|2[0-4][0-9]|25[0-5])[.]){3}(?:[0-9]|[1-9][0-9]|1[0-9][0-9]|2[0-4][0-9]|25[0-5])$/; - + /(?:^(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]\d|\d)(?:\.(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]\d|\d)){3}$)|(?:^(?:(?:[a-fA-F\d]{1,4}:){7}(?:[a-fA-F\d]{1,4}|:)|(?:[a-fA-F\d]{1,4}:){6}(?:(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]\d|\d)(?:\\.(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]\d|\d)){3}|:[a-fA-F\d]{1,4}|:)|(?:[a-fA-F\d]{1,4}:){5}(?::(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]\d|\d)(?:\\.(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]\d|\d)){3}|(?::[a-fA-F\d]{1,4}){1,2}|:)|(?:[a-fA-F\d]{1,4}:){4}(?:(?::[a-fA-F\d]{1,4}){0,1}:(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]\d|\d)(?:\\.(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]\d|\d)){3}|(?::[a-fA-F\d]{1,4}){1,3}|:)|(?:[a-fA-F\d]{1,4}:){3}(?:(?::[a-fA-F\d]{1,4}){0,2}:(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]\d|\d)(?:\\.(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]\d|\d)){3}|(?::[a-fA-F\d]{1,4}){1,4}|:)|(?:[a-fA-F\d]{1,4}:){2}(?:(?::[a-fA-F\d]{1,4}){0,3}:(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]\d|\d)(?:\\.(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]\d|\d)){3}|(?::[a-fA-F\d]{1,4}){1,5}|:)|(?:[a-fA-F\d]{1,4}:){1}(?:(?::[a-fA-F\d]{1,4}){0,4}:(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]\d|\d)(?:\\.(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]\d|\d)){3}|(?::[a-fA-F\d]{1,4}){1,6}|:)|(?::(?:(?::[a-fA-F\d]{1,4}){0,5}:(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]\d|\d)(?:\\.(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]\d|\d)){3}|(?::[a-fA-F\d]{1,4}){1,7}|:)))(?:%[0-9a-zA-Z]{1,})?$)/; return regex.test(str); } diff --git a/packages/utils/test/requestdata.test.ts b/packages/utils/test/requestdata.test.ts index 73c42a7aa545..570f80647b6b 100644 --- a/packages/utils/test/requestdata.test.ts +++ b/packages/utils/test/requestdata.test.ts @@ -1,6 +1,7 @@ import type * as net from 'net'; import type { Event, PolymorphicRequest, TransactionSource, User } from '@sentry/types'; import { addRequestDataToEvent, extractPathForTransaction, extractRequestData } from '@sentry/utils'; +import { getClientIPAddress } from '../src/vendor/getIpAddress'; describe('addRequestDataToEvent', () => { let mockEvent: Event; @@ -787,3 +788,33 @@ describe('extractPathForTransaction', () => { expect(source).toEqual('route'); }); }); + +describe('getClientIPAddress', () => { + it.each([ + [ + '2b01:cb19:8350:ed00:d0dd:fa5b:de31:8be5,2b01:cb19:8350:ed00:d0dd:fa5b:de31:8be5, 141.101.69.35', + '2b01:cb19:8350:ed00:d0dd:fa5b:de31:8be5', + ], + [ + '2b01:cb19:8350:ed00:d0dd:fa5b:de31:8be5, 2b01:cb19:8350:ed00:d0dd:fa5b:de31:8be5, 141.101.69.35', + '2b01:cb19:8350:ed00:d0dd:fa5b:de31:8be5', + ], + [ + '2a01:cb19:8350:ed00:d0dd:INVALID_IP_ADDR:8be5,141.101.69.35,2a01:cb19:8350:ed00:d0dd:fa5b:de31:8be5', + '141.101.69.35', + ], + [ + '2b01:cb19:8350:ed00:d0dd:fa5b:nope:8be5, 2b01:cb19:NOPE:ed00:d0dd:fa5b:de31:8be5, 141.101.69.35 ', + '141.101.69.35', + ], + ['2b01:cb19:8350:ed00:d0 dd:fa5b:de31:8be5, 141.101.69.35', '141.101.69.35'], + ])('should parse the IP from the X-Forwarded-For header %s', (headerValue, expectedIP) => { + const headers = { + 'X-Forwarded-For': headerValue, + }; + + const ip = getClientIPAddress(headers); + + expect(ip).toEqual(expectedIP); + }); +});