diff --git a/.changeset/serve-sim-pr-4598.md b/.changeset/serve-sim-pr-4598.md new file mode 100644 index 0000000000..327c8d680d --- /dev/null +++ b/.changeset/serve-sim-pr-4598.md @@ -0,0 +1,5 @@ +--- +'@expo/build-tools': patch +--- + +Forward captured Simulator traffic through the local egress proxy. diff --git a/packages/build-tools/src/steps/utils/__tests__/deviceSessionHost.test.ts b/packages/build-tools/src/steps/utils/__tests__/deviceSessionHost.test.ts index b1ec0c4403..3d1722c9ba 100644 --- a/packages/build-tools/src/steps/utils/__tests__/deviceSessionHost.test.ts +++ b/packages/build-tools/src/steps/utils/__tests__/deviceSessionHost.test.ts @@ -12,6 +12,7 @@ import { IosSimulatorUtils } from '../../../utils/IosSimulatorUtils'; import { verifyLocalEgressGuardAsync } from '../localEgressGuard'; import { type ServeSimApplicationOptions } from '../remoteDeviceRunSession'; +import { readLocalEgressHandoffAsync } from '../localEgress'; import { runServeSimActionAsync, stageServeSimAppAsync } from '../serveSimActions'; import { startServeSimActivityPolling } from '../serveSimActivity'; import { readServeSimServersAsync } from '../serveSimMetricsRecorder'; @@ -46,6 +47,10 @@ jest.mock('../../../utils/IosSimulatorUtils', () => ({ jest.mock('../localEgressGuard', () => ({ verifyLocalEgressGuardAsync: jest.fn() })); jest.mock('../serveSimActions'); jest.mock('../serveSimActivity'); +jest.mock('../localEgress', () => ({ + ...jest.requireActual('../localEgress'), + readLocalEgressHandoffAsync: jest.fn().mockResolvedValue(null), +})); jest.mock('../deviceRunSessionArtifacts'); jest.mock('../../../sentry'); jest.mock('../serveSimMetricsRecorder', () => ({ @@ -131,6 +136,7 @@ beforeEach(() => { jest.clearAllMocks(); jest.mocked(IosSimulatorUtils.disableApsdAsync).mockReset().mockResolvedValue(undefined); jest.mocked(verifyLocalEgressGuardAsync).mockReset().mockResolvedValue(undefined); + jest.mocked(readLocalEgressHandoffAsync).mockReset().mockResolvedValue(null); stopSessionPreview.mockResolvedValue(undefined); jest.mocked(startDeviceRunSessionPreview).mockReturnValue({ stopAsync: stopSessionPreview }); jest.mocked(ensureFfmpegInstalledOnceAsync).mockResolvedValue(undefined); @@ -1377,6 +1383,35 @@ it('uses the existing step logger for startup and shutdown output', async () => ).toBe(false); }); +it('passes the active local-egress address to serve-sim without tunnel credentials', async () => { + jest.mocked(readLocalEgressHandoffAsync).mockResolvedValue({ + port: 8899, + url: 'https://tunnel.example', + token: 'tunnel-secret', + fingerprint: 'fingerprint', + }); + const host = await startDeviceSessionHostAsync(ctx, { + runtimePlatform: BuildRuntimePlatform.DARWIN, + env, + logger, + timeoutMs: 10_000, + networkCapture: true, + }); + const spawned = jest.mocked(spawnDetached).mock.calls[0][0]; + expect(spawned.args).toEqual( + expect.arrayContaining(['--network-capture-proxy', 'http://127.0.0.1:8899']) + ); + expect(spawned.args).toContain('--network-capture'); + expect(JSON.stringify(spawned)).not.toContain('tunnel-secret'); + await host.finishAsync(); +}); + +it('does not read the iOS egress handoff for an Android host', async () => { + const host = await startHostAsync(); + expect(readLocalEgressHandoffAsync).not.toHaveBeenCalled(); + await host.finishAsync(); +}); + it.each([true, false])( 'waits for setup before launch or boot-only readiness (app: %s)', async hasApp => { diff --git a/packages/build-tools/src/steps/utils/__tests__/remoteDeviceRunSession.test.ts b/packages/build-tools/src/steps/utils/__tests__/remoteDeviceRunSession.test.ts index 64975fcf6b..0e94c21d54 100644 --- a/packages/build-tools/src/steps/utils/__tests__/remoteDeviceRunSession.test.ts +++ b/packages/build-tools/src/steps/utils/__tests__/remoteDeviceRunSession.test.ts @@ -661,6 +661,14 @@ describe(createServeSimArgs, () => { ); }); + it('passes a capture proxy as one argument, including when capture will be enabled later', () => { + const proxy = 'http://user:p%40ss%20word@proxy.example:8899'; + const args = createServeSimArgs({ port: 4321, networkCaptureProxy: proxy }); + expect(args.slice(-2)).toEqual(['--network-capture-proxy', proxy]); + expect(args).not.toContain('--network-capture'); + expect(createServeSimArgs({ port: 4321 })).not.toContain('--network-capture-proxy'); + }); + it('appends --network-capture when enabled, which also covers an already booted simulator', () => { const args = createServeSimArgs({ port: 4321, networkCapture: true }); expect(args).toContain('--network-capture'); diff --git a/packages/build-tools/src/steps/utils/deviceSessionHost.ts b/packages/build-tools/src/steps/utils/deviceSessionHost.ts index 148f5c8660..adc087fe8b 100644 --- a/packages/build-tools/src/steps/utils/deviceSessionHost.ts +++ b/packages/build-tools/src/steps/utils/deviceSessionHost.ts @@ -40,6 +40,7 @@ import { spawnDetached, startNgrokTunnelAsync, } from './remoteDeviceRunSession'; +import { LOCAL_EGRESS_PROXY_HOST, readLocalEgressHandoffAsync } from './localEgress'; import { withDeviceRunSessionTimeoutAsync } from './deviceRunSessionTimeout'; import { IosSimulatorRecordingUtils, @@ -137,6 +138,7 @@ export function createServeSimArgs({ openUrl, networkCapture = false, networkCaptureFields = [], + networkCaptureProxy, }: { port: number; turnArgs?: string[]; @@ -150,6 +152,7 @@ export function createServeSimArgs({ installAppPath?: string; networkCapture?: boolean; networkCaptureFields?: string[]; + networkCaptureProxy?: string; } & ServeSimLaunchOptions): string[] { return [ createServeSimPackageSpec(packageVersion), @@ -189,6 +192,7 @@ export function createServeSimArgs({ ...networkCaptureFields.flatMap(field => ['--network-capture-field', field]), ] : []), + ...(networkCaptureProxy !== undefined ? ['--network-capture-proxy', networkCaptureProxy] : []), ]; } @@ -496,6 +500,7 @@ async function startDeviceSessionHostInternalAsync( ? createExpoDeviceHubPackageSpec(packageVersion) : createServeSimPackageSpec(packageVersion); const turnArgs = await fetchWebPreviewTurnArgsAsync(ctx, { logger }); + const localEgress = !isAndroid ? await readLocalEgressHandoffAsync() : null; const previewExec = resolvePackageExec( resolveConfiguredPackageManager(env, PackageManager.NPM), isAndroid @@ -525,6 +530,9 @@ async function startDeviceSessionHostInternalAsync( openUrl: iosSimulatorUdid ? undefined : openUrl, networkCapture, networkCaptureFields, + networkCaptureProxy: localEgress + ? `http://${LOCAL_EGRESS_PROXY_HOST}:${localEgress.port}` + : undefined, }) ); logger.info(