From f992749f4e663ecfb84ecdf86d5c75b54c82be1c Mon Sep 17 00:00:00 2001 From: Amri Toufali Date: Thu, 11 Jun 2026 12:14:21 -0700 Subject: [PATCH] chore(auth): replace axios in auth/customs with native fetch --- packages/fxa-auth-server/config/index.ts | 19 +- packages/fxa-auth-server/lib/customs.js | 75 +-- packages/fxa-auth-server/lib/customs.spec.ts | 500 ++++++------------ packages/fxa-auth-server/lib/http-agent.ts | 51 -- packages/fxa-auth-server/package.json | 1 - .../scripts/google-events-cli.js | 26 +- yarn.lock | 1 - 7 files changed, 195 insertions(+), 478 deletions(-) delete mode 100644 packages/fxa-auth-server/lib/http-agent.ts diff --git a/packages/fxa-auth-server/config/index.ts b/packages/fxa-auth-server/config/index.ts index 12acad730f1..a0d9f90f56e 100644 --- a/packages/fxa-auth-server/config/index.ts +++ b/packages/fxa-auth-server/config/index.ts @@ -372,27 +372,12 @@ const convictConf = convict({ default: 'http://localhost:7000', env: 'CUSTOMS_SERVER_URL', }, - customsHttpAgent: { - maxSockets: { - doc: 'The maximum number of sockets to be opened per host', - default: 10000, - env: 'CUSTOMS_MAX_SOCKETS', - }, - maxFreeSockets: { - doc: 'The maximum number of free sockets to keep open for a host', - default: 10, - env: 'CUSTOMS_MAX_FREE_SOCKETS', - }, + customsClient: { timeoutMs: { - doc: 'The timeout in milliseconds for the sockets', + doc: 'Request timeout in milliseconds for calls to the customs server', default: 30000, env: 'CUSTOMS_TIMEOUT_MS', }, - freeSocketTimeoutMs: { - doc: 'The time in milliseconds for which a socket should remain open while unused', - default: 15000, - env: 'CUSTOMS_FREE_SOCKET_TIMEOUT_MS', - }, }, contentServer: { url: { diff --git a/packages/fxa-auth-server/lib/customs.js b/packages/fxa-auth-server/lib/customs.js index 8baad055969..4b78aeac5b9 100644 --- a/packages/fxa-auth-server/lib/customs.js +++ b/packages/fxa-auth-server/lib/customs.js @@ -4,10 +4,8 @@ 'use strict'; -const axios = require('axios'); const Sentry = require('@sentry/node'); const { config } = require('../config'); -const { createHttpAgent, createHttpsAgent } = require('../lib/http-agent'); const { performance } = require('perf_hooks'); const { EmailNormalization } = require('fxa-shared/email/email-normalization'); @@ -56,31 +54,14 @@ class CustomsClient { this.statsd = statsd; this.rateLimit = rateLimit; - const customsHttpAgentConfig = config.get('customsHttpAgent'); - if (url !== 'none') { - this.httpAgent = createHttpAgent( - customsHttpAgentConfig.maxSockets, - customsHttpAgentConfig.maxFreeSockets, - customsHttpAgentConfig.timeoutMs, - customsHttpAgentConfig.freeSocketTimeoutMs - ); - this.httpsAgent = createHttpsAgent( - customsHttpAgentConfig.maxSockets, - customsHttpAgentConfig.maxFreeSockets, - customsHttpAgentConfig.timeoutMs, - customsHttpAgentConfig.freeSocketTimeoutMs - ); - this.axiosInstance = axios.create({ - baseURL: url, - httpAgent: this.httpAgent, - httpsAgent: this.httpsAgent, - }); + this.url = url; + this.timeoutMs = config.get('customsClient').timeoutMs; } } async makeRequest(endpoint, requestData) { - if (!this.axiosInstance) { + if (!this.url) { return; } @@ -88,9 +69,20 @@ class CustomsClient { const startTime = performance.now(); try { - this.logHttpAgentStatus(); + const response = await fetch(`${this.url}${endpoint}`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(requestData), + signal: AbortSignal.timeout(this.timeoutMs), + }); - const response = await this.axiosInstance.post(endpoint, requestData); + // fetch does not reject on non-2xx; surface it so the catch below fails + // closed via backendServiceFailure rather than treating it as success. + if (!response.ok) { + throw new Error(`Customs server returned status ${response.status}`); + } + + const result = await response.json(); if (this.statsd) { this.statsd.timing( @@ -99,7 +91,7 @@ class CustomsClient { ); } - return response.data; + return result; } catch (err) { if (this.statsd) { this.statsd.timing( @@ -280,39 +272,6 @@ class CustomsClient { } } - logHttpAgentStatus() { - if (this.axiosInstance && this.statsd) { - this.logStatus(this.httpAgent, 'httpAgent'); - this.logStatus(this.httpsAgent, 'httpsAgent'); - } - } - - logStatus(agent, name) { - if (agent) { - const status = agent.getCurrentStatus(); - this.statsd.gauge(`${name}.createSocketCount`, status.createSocketCount); - this.statsd.gauge( - `${name}.createSocketErrorCount`, - status.createSocketErrorCount - ); - this.statsd.gauge(`${name}.closeSocketCount`, status.closeSocketCount); - this.statsd.gauge(`${name}.errorSocketCount`, status.errorSocketCount); - this.statsd.gauge( - `${name}.timeoutSocketCount`, - status.timeoutSocketCount - ); - this.statsd.gauge(`${name}.requestCount`, status.requestCount); - - Object.keys(status.freeSockets).forEach((addr, value) => { - this.statsd.gauge(`${name}.freeSockets.${addr}`, value); - }); - - Object.keys(status.sockets).forEach((addr, value) => { - this.statsd.gauge(`${name}.sockets.${addr}`, value); - }); - } - } - // #region Customs V2 v2Enabled() { return this.rateLimit != null; diff --git a/packages/fxa-auth-server/lib/customs.spec.ts b/packages/fxa-auth-server/lib/customs.spec.ts index 2fcae100ec5..577a764bfad 100644 --- a/packages/fxa-auth-server/lib/customs.spec.ts +++ b/packages/fxa-auth-server/lib/customs.spec.ts @@ -4,21 +4,24 @@ const mocks = require('../test/mocks'); const { AppError: error } = require('@fxa/accounts/errors'); -const nock = require('nock'); const CUSTOMS_URL_REAL = 'http://localhost:7000'; -const CUSTOMS_URL_MISSING = 'http://localhost:7001'; -const customsServer = nock(CUSTOMS_URL_REAL).defaultReplyHeaders({ - 'Content-Type': 'application/json', -}); const Customs = require('./customs'); const configModule = require('../config'); +// Build a minimal fetch Response stand-in for the customs server. +function customsResponse(body: any, status = 200) { + return { + ok: status >= 200 && status < 300, + status, + json: async () => body, + } as unknown as Response; +} + describe('Customs', () => { let customsNoUrl: any; let customsWithUrl: any; - let customsInvalidUrl: any; const statsd = { increment: () => {}, timing: () => {}, @@ -38,8 +41,10 @@ describe('Customs', () => { let ip_uid: string; let ip_email: string; let action: string; + let originalFetch: typeof global.fetch; beforeEach(() => { + originalFetch = global.fetch; jest.spyOn(statsd, 'increment'); jest.spyOn(statsd, 'timing'); jest.spyOn(statsd, 'gauge'); @@ -53,7 +58,7 @@ describe('Customs', () => { }); afterEach(() => { - nock.cleanAll(); + global.fetch = originalFetch; jest.restoreAllMocks(); }); @@ -74,357 +79,177 @@ describe('Customs', () => { expect(result).toBeUndefined(); }); - it('can create a customs object with a url', async () => { - customsWithUrl = new Customs(CUSTOMS_URL_REAL, log, error, statsd); - expect(customsWithUrl).toBeTruthy(); - - // Mock a check that does not get blocked. - customsServer - .post('/check', (body: any) => { - expect(body).toEqual({ - ip: ip, - email: email, - action: action, - headers: request.headers, - query: request.query, - payload: request.payload, - }); - return true; - }) - .reply(200, { block: false, retryAfter: 0 }); - - let result = await customsWithUrl.check(request, email, action); - expect(result).toBeUndefined(); + it('posts a sanitized /check body and passes when not blocked', async () => { + const customs = new Customs(CUSTOMS_URL_REAL, log, error, statsd); + global.fetch = jest + .fn() + .mockResolvedValue(customsResponse({ block: false, retryAfter: 0 })); - // flag() is now a noop - result = await customsWithUrl.flag(ip, { email, uid }); + const result = await customs.check(request, email, action); expect(result).toBeUndefined(); - // Mock a report of a password reset. - customsServer - .post('/passwordReset', (body: any) => { - expect(body).toEqual({ - ip: request.app.clientAddress, - email: email, - }); - return true; - }) - .reply(200, {}); - result = await customsWithUrl.reset(request, email); - expect(result).toBeUndefined(); + const [url, init] = (global.fetch as jest.Mock).mock.calls[0]; + expect(url).toBe(`${CUSTOMS_URL_REAL}/check`); + expect(init.method).toBe('POST'); + expect(init.headers).toEqual({ 'Content-Type': 'application/json' }); + expect(JSON.parse(init.body)).toEqual({ + ip, + email, + action, + headers: request.headers, + query: request.query, + payload: request.payload, + }); + }); - // Mock a check that does get blocked, with a retryAfter. - customsServer - .post('/check', (body: any) => { - expect(body).toEqual({ - ip: ip, - email: email, - action: action, - headers: request.headers, - query: request.query, - payload: request.payload, - }); - return true; - }) - .reply(200, { block: true, retryAfter: 10001 }); + it('throws tooManyRequests (429) when a check is blocked with a retryAfter', async () => { + const customs = new Customs(CUSTOMS_URL_REAL, log, error, statsd); + global.fetch = jest + .fn() + .mockResolvedValue(customsResponse({ block: true, retryAfter: 10001 })); + let err: any; try { - await customsWithUrl.check(request, email, action); - throw new Error( - 'This should have failed the check since it should be blocked' - ); - } catch (err: any) { - expect(err.errno).toBe(error.ERRNO.THROTTLED); - expect(err.message).toBe('Client has sent too many requests'); - expect(err.isBoom).toBeTruthy(); - expect(err.output.statusCode).toBe(429); - expect(err.output.payload.retryAfter).toBe(10001); - expect(err.output.headers['retry-after']).toBe('10001'); + await customs.check(request, email, action); + } catch (e) { + err = e; } + expect(err.errno).toBe(error.ERRNO.THROTTLED); + expect(err.message).toBe('Client has sent too many requests'); + expect(err.isBoom).toBeTruthy(); + expect(err.output.statusCode).toBe(429); + expect(err.output.payload.retryAfter).toBe(10001); + expect(err.output.headers['retry-after']).toBe('10001'); + }); - // flag() is now a noop - result = await customsWithUrl.flag(ip, { - email: email, - errno: error.ERRNO.INCORRECT_PASSWORD, - }); - expect(result).toBeUndefined(); - - // Mock a check that does get blocked, with no retryAfter. - request.headers['user-agent'] = 'test passing through headers'; - request.payload['foo'] = 'bar'; - customsServer - .post('/check', (body: any) => { - expect(body).toEqual({ - ip: ip, - email: email, - action: action, - headers: request.headers, - query: request.query, - payload: request.payload, - }); - return true; - }) - .reply(200, { block: true }); + it('throws requestBlocked (400) when a check is blocked without a retryAfter', async () => { + const customs = new Customs(CUSTOMS_URL_REAL, log, error, statsd); + global.fetch = jest + .fn() + .mockResolvedValue(customsResponse({ block: true })); + let err: any; try { - await customsWithUrl.check(request, email, action); - throw new Error( - 'This should have failed the check since it should be blocked' - ); - } catch (err: any) { - expect(err.errno).toBe(error.ERRNO.REQUEST_BLOCKED); - expect(err.message).toBe('The request was blocked for security reasons'); - expect(err.isBoom).toBeTruthy(); - expect(err.output.statusCode).toBe(400); - expect(err.output.payload.retryAfter).toBeUndefined(); - expect(err.output.headers['retry-after']).toBeUndefined(); + await customs.check(request, email, action); + } catch (e) { + err = e; } - - customsServer - .post('/checkIpOnly', (body: any) => { - expect(body).toEqual({ - ip: ip, - action: action, - }); - return true; - }) - .reply(200, { block: false, retryAfter: 0 }); - - result = await customsWithUrl.checkIpOnly(request, action); - expect(result).toBeUndefined(); + expect(err.errno).toBe(error.ERRNO.REQUEST_BLOCKED); + expect(err.message).toBe('The request was blocked for security reasons'); + expect(err.isBoom).toBeTruthy(); + expect(err.output.statusCode).toBe(400); }); - it('failed closed when creating a customs object with non-existant customs service', async () => { - customsInvalidUrl = new Customs(CUSTOMS_URL_MISSING, log, error, statsd); - expect(customsInvalidUrl).toBeTruthy(); + it('posts a sanitized /passwordReset body on reset', async () => { + const customs = new Customs(CUSTOMS_URL_REAL, log, error, statsd); + global.fetch = jest.fn().mockResolvedValue(customsResponse({})); - await expect( - customsInvalidUrl.check(request, email, action) - ).rejects.toMatchObject({ - errno: error.ERRNO.BACKEND_SERVICE_FAILURE, + await customs.reset(request, email); + + const [url, init] = (global.fetch as jest.Mock).mock.calls[0]; + expect(url).toBe(`${CUSTOMS_URL_REAL}/passwordReset`); + expect(JSON.parse(init.body)).toEqual({ + ip: request.app.clientAddress, + email, }); - await expect(customsInvalidUrl.reset(request, email)).rejects.toMatchObject( - { - errno: error.ERRNO.BACKEND_SERVICE_FAILURE, - } - ); }); - it('can rate limit checkAccountStatus /check', async () => { - customsWithUrl = new Customs(CUSTOMS_URL_REAL, log, error, statsd); - expect(customsWithUrl).toBeTruthy(); - - action = 'accountStatusCheck'; + it('posts ip and action to /checkIpOnly', async () => { + const customs = new Customs(CUSTOMS_URL_REAL, log, error, statsd); + global.fetch = jest + .fn() + .mockResolvedValue(customsResponse({ block: false, retryAfter: 0 })); - function checkRequestBody(body: any) { - expect(body).toEqual({ - ip: ip, - email: email, - action: action, - headers: request.headers, - query: request.query, - payload: request.payload, - }); - return true; - } - - customsServer - .post('/check', checkRequestBody) - .reply(200, '{"block":false,"retryAfter":0}') - .post('/check', checkRequestBody) - .reply(200, '{"block":false,"retryAfter":0}') - .post('/check', checkRequestBody) - .reply(200, '{"block":false,"retryAfter":0}') - .post('/check', checkRequestBody) - .reply(200, '{"block":false,"retryAfter":0}') - .post('/check', checkRequestBody) - .reply(200, '{"block":false,"retryAfter":0}') - .post('/check', checkRequestBody) - .reply(200, '{"block":true,"retryAfter":10001}'); - - let result; - result = await customsWithUrl.check(request, email, action); - expect(result).toBeUndefined(); - result = await customsWithUrl.check(request, email, action); - expect(result).toBeUndefined(); - result = await customsWithUrl.check(request, email, action); - expect(result).toBeUndefined(); - result = await customsWithUrl.check(request, email, action); - expect(result).toBeUndefined(); - result = await customsWithUrl.check(request, email, action); + const result = await customs.checkIpOnly(request, action); expect(result).toBeUndefined(); - try { - await customsWithUrl.check(request, email, action); - throw new Error( - 'This should have failed the check since it should be blocked' - ); - } catch (err: any) { - expect(err.errno).toBe(114); - expect(err.message).toBe('Client has sent too many requests'); - expect(err.isBoom).toBeTruthy(); - expect(err.output.statusCode).toBe(429); - expect(err.output.payload.retryAfter).toBe(10001); - expect(err.output.payload.retryAfterLocalized).toBe('in 3 hours'); - expect(err.output.headers['retry-after']).toBe('10001'); - } + const [url, init] = (global.fetch as jest.Mock).mock.calls[0]; + expect(url).toBe(`${CUSTOMS_URL_REAL}/checkIpOnly`); + expect(JSON.parse(init.body)).toEqual({ ip, action }); }); - it('can rate limit devicesNotify /checkAuthenticated', async () => { - customsWithUrl = new Customs(CUSTOMS_URL_REAL, log, error, statsd); - expect(customsWithUrl).toBeTruthy(); + it('treats flag() as a no-op', async () => { + const customs = new Customs(CUSTOMS_URL_REAL, log, error, statsd); + expect(await customs.flag(ip, { email, uid })).toBeUndefined(); + }); - action = 'devicesNotify'; - const uid = 'foo'; - const email = 'bar@mozilla.com'; - - function checkRequestBody(body: any) { - expect(body).toEqual({ - action: action, - ip: ip, - uid: uid, - }); - return true; - } + it('fails closed (backendServiceFailure) when the request rejects', async () => { + const customs = new Customs(CUSTOMS_URL_REAL, log, error, statsd); + global.fetch = jest.fn().mockRejectedValue(new Error('ECONNREFUSED')); - customsServer - .post('/checkAuthenticated', checkRequestBody) - .reply(200, '{"block":false,"retryAfter":0}') - .post('/checkAuthenticated', checkRequestBody) - .reply(200, '{"block":false,"retryAfter":0}') - .post('/checkAuthenticated', checkRequestBody) - .reply(200, '{"block":false,"retryAfter":0}') - .post('/checkAuthenticated', checkRequestBody) - .reply(200, '{"block":false,"retryAfter":0}') - .post('/checkAuthenticated', checkRequestBody) - .reply(200, '{"block":false,"retryAfter":0}') - .post('/checkAuthenticated', checkRequestBody) - .reply(200, '{"block":true,"retryAfter":10001}'); - - let result; - result = await customsWithUrl.checkAuthenticated( - request, - uid, - email, - action - ); - expect(result).toBeUndefined(); - result = await customsWithUrl.checkAuthenticated( - request, - uid, - email, - action - ); - expect(result).toBeUndefined(); - result = await customsWithUrl.checkAuthenticated( - request, - uid, - email, - action - ); - expect(result).toBeUndefined(); - result = await customsWithUrl.checkAuthenticated( - request, - uid, - email, - action - ); - expect(result).toBeUndefined(); - result = await customsWithUrl.checkAuthenticated( - request, - uid, - email, - action - ); - expect(result).toBeUndefined(); + await expect(customs.check(request, email, action)).rejects.toMatchObject({ + errno: error.ERRNO.BACKEND_SERVICE_FAILURE, + }); + await expect(customs.reset(request, email)).rejects.toMatchObject({ + errno: error.ERRNO.BACKEND_SERVICE_FAILURE, + }); + }); - try { - await customsWithUrl.checkAuthenticated(request, uid, email, action); - throw new Error( - 'This should have failed the check since it should be blocked' - ); - } catch (err: any) { - expect(err.errno).toBe(114); - expect(err.message).toBe('Client has sent too many requests'); - expect(err.isBoom).toBeTruthy(); - expect(err.output.statusCode).toBe(429); - expect(err.output.payload.retryAfter).toBe(10001); - expect(err.output.headers['retry-after']).toBe('10001'); - } + it('fails closed (backendServiceFailure) on a non-ok response', async () => { + const customs = new Customs(CUSTOMS_URL_REAL, log, error, statsd); + global.fetch = jest.fn().mockResolvedValue(customsResponse('error', 500)); + + await expect(customs.check(request, email, action)).rejects.toMatchObject({ + errno: error.ERRNO.BACKEND_SERVICE_FAILURE, + }); }); - it('can rate limit verifyTotpCode /check', async () => { - action = 'verifyTotpCode'; - email = 'test@email.com'; - - customsWithUrl = new Customs(CUSTOMS_URL_REAL, log, error, statsd); - expect(customsWithUrl).toBeTruthy(); - - function checkRequestBody(body: any) { - expect(body).toEqual({ - ip: ip, - email: email, - action: action, - headers: request.headers, - query: request.query, - payload: request.payload, - }); - return true; - } + it('makes a fresh request for each check rather than caching', async () => { + const customs = new Customs(CUSTOMS_URL_REAL, log, error, statsd); + global.fetch = jest + .fn() + .mockResolvedValueOnce(customsResponse({ block: false, retryAfter: 0 })) + .mockResolvedValueOnce( + customsResponse({ block: true, retryAfter: 10001 }) + ); - customsServer - .post('/check', checkRequestBody) - .reply(200, '{"block":false,"retryAfter":0}') - .post('/check', checkRequestBody) - .reply(200, '{"block":false,"retryAfter":0}') - .post('/check', checkRequestBody) - .reply(200, '{"block":true,"retryAfter":30}'); + await customs.check(request, email, action); + await expect(customs.check(request, email, action)).rejects.toMatchObject({ + output: { statusCode: 429 }, + }); + expect(global.fetch).toHaveBeenCalledTimes(2); + }); - let result; - result = await customsWithUrl.check(request, email, action); - expect(result).toBeUndefined(); - result = await customsWithUrl.check(request, email, action); - expect(result).toBeUndefined(); + it('posts a sanitized /checkAuthenticated body and throws 429 when blocked', async () => { + const customs = new Customs(CUSTOMS_URL_REAL, log, error, statsd); + const authUid = 'foo'; + const authEmail = 'bar@mozilla.com'; + action = 'devicesNotify'; + global.fetch = jest + .fn() + .mockResolvedValue(customsResponse({ block: true, retryAfter: 10001 })); + let err: any; try { - await customsWithUrl.check(request, email, action); - throw new Error('should have been blocked'); - } catch (err: any) { - expect(err.errno).toBe(114); - expect(err.message).toBe('Client has sent too many requests'); - expect(err.isBoom).toBeTruthy(); - expect(err.output.statusCode).toBe(429); - expect(err.output.payload.retryAfter).toBe(30); - expect(err.output.headers['retry-after']).toBe('30'); + await customs.checkAuthenticated(request, authUid, authEmail, action); + } catch (e) { + err = e; } + expect(err.output.statusCode).toBe(429); + expect(err.output.payload.retryAfter).toBe(10001); + expect(err.output.payload.retryAfterLocalized).toBe('in 3 hours'); + + const [url, init] = (global.fetch as jest.Mock).mock.calls[0]; + expect(url).toBe(`${CUSTOMS_URL_REAL}/checkAuthenticated`); + expect(JSON.parse(init.body)).toEqual({ action, ip, uid: authUid }); }); - it('can scrub customs request object', async () => { - customsWithUrl = new Customs(CUSTOMS_URL_REAL, log, error, statsd); - expect(customsWithUrl).toBeTruthy(); - - request.payload.authPW = 'asdfasdfadsf'; - request.payload.oldAuthPW = '012301230123'; - request.payload.notThePW = 'plaintext'; - - customsServer - .post('/check', (body: any) => { - expect(body).toEqual({ - ip: ip, - email: email, - action: action, - headers: request.headers, - query: request.query, - payload: { - notThePW: 'plaintext', - }, - }); - return true; - }) - .reply(200, { block: false, retryAfter: 0 }); + describe('sanitizePayload', () => { + it('strips sensitive fields (authPW, oldAuthPW, paymentToken)', () => { + const customs = new Customs('none', log, error, statsd); + const sanitized = customs.sanitizePayload({ + authPW: 'secret', + oldAuthPW: 'old-secret', + paymentToken: 'token', + notThePW: 'plaintext', + }); + expect(sanitized).toEqual({ notThePW: 'plaintext' }); + }); - const result = await customsWithUrl.check(request, email, action); - expect(result).toBeUndefined(); + it('returns undefined when there is no payload', () => { + const customs = new Customs('none', log, error, statsd); + expect(customs.sanitizePayload(undefined)).toBeUndefined(); + }); }); describe('customs v2', () => { @@ -535,7 +360,11 @@ describe('Customs', () => { await customs.check(request, email, 'accountStatusCheck'); - expect(mockRateLimit.skip).toHaveBeenCalledWith('accountStatusCheck', { ip, email, ip_email }); + expect(mockRateLimit.skip).toHaveBeenCalledWith('accountStatusCheck', { + ip, + email, + ip_email, + }); expect(mockRateLimit.check).toHaveBeenCalledTimes(0); }); @@ -636,7 +465,7 @@ describe('Customs', () => { }); it('reports for /check', async () => { - customsServer.post('/check').reply(200, tags); + global.fetch = jest.fn().mockResolvedValue(customsResponse(tags)); await expect( customsWithUrl.check(request, email, action) @@ -649,18 +478,10 @@ describe('Customs', () => { expect.stringContaining('customs.check.success'), expect.anything() ); - expect(statsd.gauge).toHaveBeenCalledWith( - expect.stringContaining('httpAgent.createSocketCount'), - expect.anything() - ); - expect(statsd.gauge).toHaveBeenCalledWith( - expect.stringContaining('httpsAgent.createSocketCount'), - expect.anything() - ); }); it('reports for /checkIpOnly', async () => { - customsServer.post('/checkIpOnly').reply(200, tags); + global.fetch = jest.fn().mockResolvedValue(customsResponse(tags)); await expect( customsWithUrl.checkIpOnly(request, action) @@ -679,10 +500,11 @@ describe('Customs', () => { }); it('reports for /checkAuthenticated', async () => { - customsServer.post('/checkAuthenticated').reply(200, { - block: true, - blockReason: 'other', - }); + global.fetch = jest + .fn() + .mockResolvedValue( + customsResponse({ block: true, blockReason: 'other' }) + ); await expect( customsWithUrl.checkAuthenticated( @@ -707,7 +529,7 @@ describe('Customs', () => { }); it('reports failure statsd timing', async () => { - customsServer.post('/check').reply(400, tags); + global.fetch = jest.fn().mockResolvedValue(customsResponse(tags, 400)); await expect( customsWithUrl.check(request, email, action) ).rejects.toThrow(); diff --git a/packages/fxa-auth-server/lib/http-agent.ts b/packages/fxa-auth-server/lib/http-agent.ts deleted file mode 100644 index 143f4bcada3..00000000000 --- a/packages/fxa-auth-server/lib/http-agent.ts +++ /dev/null @@ -1,51 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -import Agent from 'agentkeepalive'; - -/** - * Creates an HTTP agent using the `agentkeepalive` library. - * - * @param {number} [maxSockets=100] - The maximum number of sockets to be opened per host. - * @param {number} [maxFreeSockets=10] - The maximum number of free sockets to keep open for a host. - * @param {number} [timeoutMs=60000] - The timeout in milliseconds for the sockets. - * @param {number} [freeSocketTimeoutMs=30000] - The time in milliseconds for which a socket should remain open while unused. - * @returns {Agent} An instance of Agent, configured with the specified settings. - */ -export function createHttpAgent( - maxSockets = 1000, - maxFreeSockets = 10, - timeoutMs = 30000, - freeSocketTimeoutMs = 15000 -) { - return new Agent({ - maxSockets, - maxFreeSockets, - timeout: timeoutMs, - freeSocketTimeout: freeSocketTimeoutMs, - }); -} - -/** - * Creates an HTTPS agent using the `agentkeepalive` library. - * - * @param {number} [maxSockets=100] - The maximum number of sockets to be opened per host for HTTPS requests. - * @param {number} [maxFreeSockets=10] - The maximum number of free sockets to keep open for a host for HTTPS requests. - * @param {number} [timeoutMs=60000] - The timeout in milliseconds for the sockets for HTTPS requests. - * @param {number} [freeSocketTimeoutMs=30000] - The time in milliseconds for which a socket should remain open while unused for HTTPS requests. - * @returns {Agent.HttpsAgent} An instance of Agent.HttpsAgent, configured with the specified settings. - */ -export function createHttpsAgent( - maxSockets = 1000, - maxFreeSockets = 10, - timeoutMs = 30000, - freeSocketTimeoutMs = 15000 -) { - return new Agent.HttpsAgent({ - maxSockets, - maxFreeSockets, - timeout: timeoutMs, - freeSocketTimeout: freeSocketTimeoutMs, - }); -} diff --git a/packages/fxa-auth-server/package.json b/packages/fxa-auth-server/package.json index f73a829a805..d7cfb4472b5 100644 --- a/packages/fxa-auth-server/package.json +++ b/packages/fxa-auth-server/package.json @@ -72,7 +72,6 @@ "@types/convict": "5.2.2", "@types/ejs": "^3.0.6", "@types/mjml": "^4.7.4", - "agentkeepalive": "^4.6.0", "ajv": "^8.20.0", "commander": "2.18.0", "convict": "^6.2.5", diff --git a/packages/fxa-auth-server/scripts/google-events-cli.js b/packages/fxa-auth-server/scripts/google-events-cli.js index db31684bb7a..f18115ca730 100644 --- a/packages/fxa-auth-server/scripts/google-events-cli.js +++ b/packages/fxa-auth-server/scripts/google-events-cli.js @@ -11,7 +11,6 @@ */ const fs = require('fs'); const jwt = require('jsonwebtoken'); -const axios = require('axios'); const { Command } = require('commander'); const program = new Command(); @@ -88,14 +87,15 @@ async function getEventStreamConfig(authToken) { }; try { - const response = await axios.get(streamUpdateEndpoint, { + const response = await fetch(streamUpdateEndpoint, { headers: headers, }); if (response.status !== 200) { throw new Error(`Request failed with status ${response.status}`); } - console.log('getEventStreamConfig response', response.data); - return response.data; + const data = await response.json(); + console.log('getEventStreamConfig response', data); + return data; } catch (error) { console.error(`Error configuring the event stream: ${error}`); throw error; @@ -127,15 +127,17 @@ async function configureEventStream( ...streamCfg, }; - const response = await axios.post(streamUpdateEndpoint, newConfig, { - headers: headers, + const response = await fetch(streamUpdateEndpoint, { + method: 'POST', + headers: { ...headers, 'Content-Type': 'application/json' }, + body: JSON.stringify(newConfig), }); - console.log('configureEventStream response', response.data); if (response.status !== 200) { throw new Error(`Request failed with status ${response.status}`); } + console.log('configureEventStream response', await response.json()); } catch (error) { - console.log(`Error configuring the event stream: ${error}`); + console.error(`Error configuring the event stream: ${error}`); throw error; } } @@ -151,13 +153,15 @@ async function testEventStream(authToken, nonce) { }; try { - const response = await axios.post(streamVerifyEndpoint, state, { - headers: headers, + const response = await fetch(streamVerifyEndpoint, { + method: 'POST', + headers: { ...headers, 'Content-Type': 'application/json' }, + body: JSON.stringify(state), }); - console.log('testEventStream response', response.data); if (response.status !== 200) { throw new Error(`Request failed with status ${response.status}`); } + console.log('testEventStream response', await response.json()); } catch (error) { console.error(`Error testing the event stream: ${error.message}`); throw error; diff --git a/yarn.lock b/yarn.lock index f06706aa6d1..6df58a3663f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -31006,7 +31006,6 @@ __metadata: "@types/uuid": "npm:^10.0.0" "@types/verror": "npm:^1.10.4" "@types/webpack": "npm:5.28.5" - agentkeepalive: "npm:^4.6.0" ajv: "npm:^8.20.0" async-retry: "npm:^1.3.3" audit-filter: "npm:^0.5.0"