@expo/build-tools 24.2.0 → 24.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/common/projectSources.js +3 -0
- package/dist/steps/easFunctions.js +2 -0
- package/dist/steps/functions/startIosSimulator.js +34 -2
- package/dist/steps/functions/startLocalEgress.d.ts +4 -3
- package/dist/steps/functions/startLocalEgress.js +10 -6
- package/dist/steps/functions/startSandbox.d.ts +4 -0
- package/dist/steps/functions/startSandbox.js +93 -0
- package/dist/steps/utils/localEgress.d.ts +24 -0
- package/dist/steps/utils/localEgress.js +59 -1
- package/dist/steps/utils/localEgressGuard.d.ts +179 -0
- package/dist/steps/utils/localEgressGuard.js +537 -0
- package/dist/steps/utils/localEgressSession.js +4 -0
- package/dist/steps/utils/sandboxDaemon.d.ts +13 -0
- package/dist/steps/utils/sandboxDaemon.js +90 -0
- package/dist/utils/IosSimulatorUtils.d.ts +34 -0
- package/dist/utils/IosSimulatorUtils.js +62 -0
- package/package.json +8 -4
- package/resources/egress-guard/README.md +83 -0
- package/resources/egress-guard/build.sh +30 -0
- package/resources/egress-guard/check.c +112 -0
- package/resources/egress-guard/guard.c +274 -0
- package/resources/egress-guard/policy.c +164 -0
- package/resources/egress-guard/policy.h +60 -0
- package/resources/egress-guard/tests/guard_insert_test.c +291 -0
- package/resources/egress-guard/tests/guard_test.c +177 -0
- package/resources/egress-guard/tests/nettest.swift +172 -0
- package/resources/egress-guard/tests/policy_test.c +143 -0
- package/resources/egress-guard/tests/run-guard-tests.sh +62 -0
- package/resources/egress-guard/tests/run-policy-tests.sh +7 -0
|
@@ -19,6 +19,9 @@ const zod_1 = require("zod");
|
|
|
19
19
|
const git_1 = require("./git");
|
|
20
20
|
const turtleFetch_1 = require("../utils/turtleFetch");
|
|
21
21
|
async function prepareProjectSourcesAsync(ctx, destinationDirectory) {
|
|
22
|
+
if (ctx.job.projectArchive.type === eas_build_job_1.ArchiveSourceType.NONE) {
|
|
23
|
+
return { handled: true };
|
|
24
|
+
}
|
|
22
25
|
if (ctx.isLocal) {
|
|
23
26
|
if (ctx.job.projectArchive.type !== eas_build_job_1.ArchiveSourceType.PATH) {
|
|
24
27
|
throw new eas_build_job_1.SystemError(`Expected a PATH project source for a local build, received ${ctx.job.projectArchive.type}.`, { trackingCode: 'INVALID_LOCAL_PROJECT_SOURCE' });
|
|
@@ -52,6 +52,7 @@ const startIosSimulatorRecordings_1 = require("./functions/startIosSimulatorReco
|
|
|
52
52
|
const startLocalEgress_1 = require("./functions/startLocalEgress");
|
|
53
53
|
const startWebPreviewRemoteSession_1 = require("./functions/startWebPreviewRemoteSession");
|
|
54
54
|
const startServeSimMetrics_1 = require("./functions/startServeSimMetrics");
|
|
55
|
+
const startSandbox_1 = require("./functions/startSandbox");
|
|
55
56
|
const collectServeSimMetrics_1 = require("./functions/collectServeSimMetrics");
|
|
56
57
|
const uploadArtifact_1 = require("./functions/uploadArtifact");
|
|
57
58
|
const uploadDeviceRunSessionScreenRecordings_1 = require("./functions/uploadDeviceRunSessionScreenRecordings");
|
|
@@ -106,6 +107,7 @@ function getEasFunctions(ctx) {
|
|
|
106
107
|
(0, uploadDeviceRunSessionScreenRecordings_1.createUploadDeviceRunSessionScreenRecordingsBuildFunction)(ctx),
|
|
107
108
|
(0, startWebPreviewRemoteSession_1.createStartWebPreviewRemoteSessionBuildFunction)(ctx),
|
|
108
109
|
(0, startServeSimMetrics_1.createStartServeSimMetricsBuildFunction)(),
|
|
110
|
+
(0, startSandbox_1.createStartSandboxBuildFunction)(ctx),
|
|
109
111
|
(0, collectServeSimMetrics_1.createCollectServeSimMetricsBuildFunction)(ctx),
|
|
110
112
|
(0, installMaestro_1.createInstallMaestroBuildFunction)(),
|
|
111
113
|
(0, installPods_1.createInstallPodsBuildFunction)(),
|
|
@@ -7,6 +7,8 @@ exports.createStartIosSimulatorBuildFunction = createStartIosSimulatorBuildFunct
|
|
|
7
7
|
const steps_1 = require("@expo/steps");
|
|
8
8
|
const turtle_spawn_1 = __importDefault(require("@expo/turtle-spawn"));
|
|
9
9
|
const lodash_1 = require("lodash");
|
|
10
|
+
const localEgress_1 = require("../utils/localEgress");
|
|
11
|
+
const localEgressGuard_1 = require("../utils/localEgressGuard");
|
|
10
12
|
const IosSimulatorUtils_1 = require("../../utils/IosSimulatorUtils");
|
|
11
13
|
function createStartIosSimulatorBuildFunction() {
|
|
12
14
|
return new steps_1.BuildFunction({
|
|
@@ -61,9 +63,10 @@ function createStartIosSimulatorBuildFunction() {
|
|
|
61
63
|
env,
|
|
62
64
|
});
|
|
63
65
|
}
|
|
64
|
-
const
|
|
66
|
+
const udid = await bootWithLocalEgressAsync({
|
|
65
67
|
deviceIdentifier: originalDeviceIdentifier,
|
|
66
68
|
env,
|
|
69
|
+
logger,
|
|
67
70
|
});
|
|
68
71
|
try {
|
|
69
72
|
await IosSimulatorUtils_1.IosSimulatorUtils.disableApsdAsync({ udid, env });
|
|
@@ -97,9 +100,10 @@ function createStartIosSimulatorBuildFunction() {
|
|
|
97
100
|
env,
|
|
98
101
|
});
|
|
99
102
|
}
|
|
100
|
-
const
|
|
103
|
+
const cloneUdid = await bootWithLocalEgressAsync({
|
|
101
104
|
deviceIdentifier: cloneDeviceName,
|
|
102
105
|
env,
|
|
106
|
+
logger,
|
|
103
107
|
});
|
|
104
108
|
try {
|
|
105
109
|
await IosSimulatorUtils_1.IosSimulatorUtils.disableApsdAsync({ udid: cloneUdid, env });
|
|
@@ -118,6 +122,34 @@ function createStartIosSimulatorBuildFunction() {
|
|
|
118
122
|
},
|
|
119
123
|
});
|
|
120
124
|
}
|
|
125
|
+
/**
|
|
126
|
+
* Boot a device with the local egress environment in place before anything
|
|
127
|
+
* inside it starts. `simctl boot` returns once the simulator's launchd is up
|
|
128
|
+
* and before it has spawned anything else, which is the only moment at which
|
|
129
|
+
* launchd environment reaches every process of the boot. The proxy variables
|
|
130
|
+
* and the guard are set in that gap; then the boot is waited for and the
|
|
131
|
+
* guard is verified in a fresh process. Nothing here applies when no local
|
|
132
|
+
* egress session is active.
|
|
133
|
+
*/
|
|
134
|
+
async function bootWithLocalEgressAsync({ deviceIdentifier, env, logger, }) {
|
|
135
|
+
const udid = await IosSimulatorUtils_1.IosSimulatorUtils.resolveUdidAsync({ deviceIdentifier, env });
|
|
136
|
+
// The guard and proxy variables ride the boot itself, so launchd has them
|
|
137
|
+
// before it spawns its first process.
|
|
138
|
+
const launchdEnvironment = await (0, localEgressGuard_1.resolveLocalEgressBootEnvironmentAsync)();
|
|
139
|
+
await IosSimulatorUtils_1.IosSimulatorUtils.bootAsync({
|
|
140
|
+
deviceIdentifier: udid,
|
|
141
|
+
env,
|
|
142
|
+
launchdEnvironment: launchdEnvironment ?? {},
|
|
143
|
+
});
|
|
144
|
+
// Also set them through launchctl, for a device that was already booted.
|
|
145
|
+
const guardInstalled = await (0, localEgressGuard_1.installLocalEgressGuardAsync)({ udid, env, logger });
|
|
146
|
+
await (0, localEgress_1.configureSimulatorProxyEnvironmentAsync)({ udid, env, logger });
|
|
147
|
+
await IosSimulatorUtils_1.IosSimulatorUtils.startAsync({ deviceIdentifier: udid, env });
|
|
148
|
+
if (guardInstalled) {
|
|
149
|
+
await (0, localEgressGuard_1.verifyLocalEgressGuardAsync)({ udid, env, logger });
|
|
150
|
+
}
|
|
151
|
+
return udid;
|
|
152
|
+
}
|
|
121
153
|
async function findMostGenericIphoneUuidAsync({ env, }) {
|
|
122
154
|
const availableSimulatorDevices = await IosSimulatorUtils_1.IosSimulatorUtils.getAvailableDevicesAsync({
|
|
123
155
|
env,
|
|
@@ -2,9 +2,10 @@ import { BuildFunction } from '@expo/steps';
|
|
|
2
2
|
/**
|
|
3
3
|
* Points the device host's system HTTP(S) proxy at the EAS CLI's egress client.
|
|
4
4
|
* Must run before `eas/start_ios_simulator`: the simulator reads the system proxy
|
|
5
|
-
* at boot.
|
|
6
|
-
*
|
|
7
|
-
*
|
|
5
|
+
* at boot. Once a simulator boots, `eas/start_ios_simulator` also sets proxy
|
|
6
|
+
* environment variables inside it for clients that read them (gRPC, libcurl)
|
|
7
|
+
* and installs the local egress guard, which refuses connections that ignore
|
|
8
|
+
* both. The shared session cleanup releases the resources started here when
|
|
8
9
|
* the session ends, with a job finalizer as a fallback.
|
|
9
10
|
*/
|
|
10
11
|
export declare function createStartLocalEgressBuildFunction(): BuildFunction;
|
|
@@ -9,6 +9,7 @@ const node_fs_1 = __importDefault(require("node:fs"));
|
|
|
9
9
|
const node_os_1 = __importDefault(require("node:os"));
|
|
10
10
|
const node_path_1 = __importDefault(require("node:path"));
|
|
11
11
|
const localEgress_1 = require("../utils/localEgress");
|
|
12
|
+
const localEgressGuard_1 = require("../utils/localEgressGuard");
|
|
12
13
|
const remoteDeviceRunSession_1 = require("../utils/remoteDeviceRunSession");
|
|
13
14
|
/**
|
|
14
15
|
* The downloader and ngrok SDK do not accept an AbortSignal. Stop awaiting them
|
|
@@ -50,9 +51,10 @@ function awaitLocalEgressAcquisitionAsync(pending, signal, disposeAbandonedAsync
|
|
|
50
51
|
/**
|
|
51
52
|
* Points the device host's system HTTP(S) proxy at the EAS CLI's egress client.
|
|
52
53
|
* Must run before `eas/start_ios_simulator`: the simulator reads the system proxy
|
|
53
|
-
* at boot.
|
|
54
|
-
*
|
|
55
|
-
*
|
|
54
|
+
* at boot. Once a simulator boots, `eas/start_ios_simulator` also sets proxy
|
|
55
|
+
* environment variables inside it for clients that read them (gRPC, libcurl)
|
|
56
|
+
* and installs the local egress guard, which refuses connections that ignore
|
|
57
|
+
* both. The shared session cleanup releases the resources started here when
|
|
56
58
|
* the session ends, with a job finalizer as a fallback.
|
|
57
59
|
*/
|
|
58
60
|
function createStartLocalEgressBuildFunction() {
|
|
@@ -77,6 +79,7 @@ function createStartLocalEgressBuildFunction() {
|
|
|
77
79
|
const lifetimeSignal = (0, localEgress_1.registerLocalEgressResources)(async () => {
|
|
78
80
|
await setupFinished;
|
|
79
81
|
const results = await Promise.allSettled([
|
|
82
|
+
Promise.resolve().then(() => (0, localEgressGuard_1.stopLocalEgressGuardRelaysAsync)(logger)),
|
|
80
83
|
Promise.resolve().then(() => tunnel?.stopAsync()),
|
|
81
84
|
Promise.resolve().then(() => server?.stopAsync()),
|
|
82
85
|
]);
|
|
@@ -139,9 +142,10 @@ function createStartLocalEgressBuildFunction() {
|
|
|
139
142
|
startupSignal.throwIfAborted();
|
|
140
143
|
logger.info(`Local egress is configured on network service "${service}". HTTP(S) and WebSocket ` +
|
|
141
144
|
'requests that honor the system proxy (WebKit, URLSession and other CFNetwork clients) ' +
|
|
142
|
-
'fail until the EAS CLI egress client connects, then exit from that machine.
|
|
143
|
-
'
|
|
144
|
-
'
|
|
145
|
+
'fail until the EAS CLI egress client connects, then exit from that machine. Once the ' +
|
|
146
|
+
'Simulator boots, proxy environment variables are set inside it for clients that read ' +
|
|
147
|
+
'them (gRPC, libcurl), and the local egress guard is installed so that connections which ' +
|
|
148
|
+
'ignore both are refused in the process that makes them and reported here.');
|
|
145
149
|
}
|
|
146
150
|
catch (error) {
|
|
147
151
|
finishSetup();
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import { BuildFunction } from '@expo/steps';
|
|
2
|
+
import { CustomBuildContext } from '../../customBuildContext';
|
|
3
|
+
export declare function createStartSandboxBuildFunction(ctx: CustomBuildContext): BuildFunction;
|
|
4
|
+
export declare function markSandboxReadyAsync(ctx: CustomBuildContext, sandboxId: string, signal?: AbortSignal): Promise<void>;
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.createStartSandboxBuildFunction = createStartSandboxBuildFunction;
|
|
4
|
+
exports.markSandboxReadyAsync = markSandboxReadyAsync;
|
|
5
|
+
const eas_build_job_1 = require("@expo/eas-build-job");
|
|
6
|
+
const steps_1 = require("@expo/steps");
|
|
7
|
+
const gql_tada_1 = require("gql.tada");
|
|
8
|
+
const logPhase_1 = require("../../utils/logPhase");
|
|
9
|
+
const sandboxDaemon_1 = require("../utils/sandboxDaemon");
|
|
10
|
+
const MARK_SANDBOX_READY_MUTATION = (0, gql_tada_1.graphql)(`
|
|
11
|
+
mutation MarkSandboxReady($sandboxId: ID!) {
|
|
12
|
+
sandbox {
|
|
13
|
+
markSandboxReady(sandboxId: $sandboxId) {
|
|
14
|
+
id
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
`);
|
|
19
|
+
const RECONNECT_DELAY_MS = 1_000;
|
|
20
|
+
const AWAIT_SANDBOX_STEP_NAME = 'Await sandbox completion';
|
|
21
|
+
function createStartSandboxBuildFunction(ctx) {
|
|
22
|
+
return new steps_1.BuildFunction({
|
|
23
|
+
namespace: 'eas',
|
|
24
|
+
id: 'start_sandbox',
|
|
25
|
+
name: 'Start sandbox daemon',
|
|
26
|
+
__metricsId: 'eas/start_sandbox',
|
|
27
|
+
inputProviders: [
|
|
28
|
+
steps_1.BuildStepInput.createProvider({
|
|
29
|
+
id: 'sandbox_id',
|
|
30
|
+
required: true,
|
|
31
|
+
allowedValueTypeName: steps_1.BuildStepInputValueTypeName.STRING,
|
|
32
|
+
}),
|
|
33
|
+
],
|
|
34
|
+
fn: async (stepCtx, { inputs, signal }) => {
|
|
35
|
+
const sandboxToken = ctx.env.__EAS_SANDBOX_MCP_TOKEN;
|
|
36
|
+
if (!sandboxToken) {
|
|
37
|
+
throw new eas_build_job_1.SystemError('__EAS_SANDBOX_MCP_TOKEN is required to start the sandbox daemon.');
|
|
38
|
+
}
|
|
39
|
+
const mcpServerUrl = ctx.mcpServerUrl;
|
|
40
|
+
if (!mcpServerUrl) {
|
|
41
|
+
throw new eas_build_job_1.SystemError('MCP server URL is required to start the sandbox daemon.');
|
|
42
|
+
}
|
|
43
|
+
const sandboxId = String(inputs.sandbox_id.value);
|
|
44
|
+
const daemon = await (0, sandboxDaemon_1.startSandboxDaemonAsync)({
|
|
45
|
+
credential: sandboxToken,
|
|
46
|
+
serverUrl: mcpServerUrl,
|
|
47
|
+
reconnectDelayMs: RECONNECT_DELAY_MS,
|
|
48
|
+
logger: stepCtx.logger,
|
|
49
|
+
signal,
|
|
50
|
+
});
|
|
51
|
+
try {
|
|
52
|
+
await daemon.ready;
|
|
53
|
+
signal?.throwIfAborted();
|
|
54
|
+
await markSandboxReadyAsync(ctx, sandboxId, signal);
|
|
55
|
+
signal?.throwIfAborted();
|
|
56
|
+
stepCtx.logger.info('Sandbox daemon started.');
|
|
57
|
+
await (0, logPhase_1.withLogPhaseAsync)(stepCtx.logger, AWAIT_SANDBOX_STEP_NAME, async () => {
|
|
58
|
+
await waitForCancellationAsync(signal);
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
finally {
|
|
62
|
+
await daemon.stopAsync();
|
|
63
|
+
}
|
|
64
|
+
},
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
async function markSandboxReadyAsync(ctx, sandboxId, signal) {
|
|
68
|
+
signal?.throwIfAborted();
|
|
69
|
+
const result = await ctx.graphqlClient
|
|
70
|
+
.mutation(MARK_SANDBOX_READY_MUTATION, { sandboxId }, signal
|
|
71
|
+
? {
|
|
72
|
+
fetch: (input, init) => fetch(input, {
|
|
73
|
+
...init,
|
|
74
|
+
signal: init?.signal ? AbortSignal.any([signal, init.signal]) : signal,
|
|
75
|
+
}),
|
|
76
|
+
}
|
|
77
|
+
: undefined)
|
|
78
|
+
.toPromise();
|
|
79
|
+
signal?.throwIfAborted();
|
|
80
|
+
if (result.error) {
|
|
81
|
+
throw new eas_build_job_1.SystemError(`Failed to mark sandbox ${sandboxId} as ready.`, {
|
|
82
|
+
cause: result.error,
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
async function waitForCancellationAsync(signal) {
|
|
87
|
+
if (signal?.aborted) {
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
await new Promise(resolve => {
|
|
91
|
+
signal?.addEventListener('abort', () => resolve(), { once: true });
|
|
92
|
+
});
|
|
93
|
+
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { bunyan } from '@expo/logger';
|
|
2
2
|
import { BuildStepEnv } from '@expo/steps';
|
|
3
|
+
import { type IosSimulatorUuid } from '../../utils/IosSimulatorUtils';
|
|
3
4
|
import { type DetachedProcessHandle } from './remoteDeviceRunSession';
|
|
4
5
|
/**
|
|
5
6
|
* Local egress provides an HTTP(S) proxy through the machine running the EAS CLI.
|
|
@@ -101,6 +102,29 @@ export declare function configureSystemProxyAsync({ env, logger, port, signal, }
|
|
|
101
102
|
}): Promise<{
|
|
102
103
|
service: string;
|
|
103
104
|
}>;
|
|
105
|
+
export declare const LOCAL_EGRESS_NO_PROXY = "localhost,127.0.0.1,::1";
|
|
106
|
+
/**
|
|
107
|
+
* Proxy environment for processes inside the simulator. CFNetwork ignores these
|
|
108
|
+
* variables and follows the system proxy, but clients with their own network
|
|
109
|
+
* stack that read them (gRPC and therefore Firestore, libcurl, Go) then send
|
|
110
|
+
* HTTP CONNECT requests to the same loopback port. Loopback is excluded so the
|
|
111
|
+
* ports forwarded for `--egress-allow` behave the same on both paths. Lowercase
|
|
112
|
+
* and uppercase forms are both set because clients differ in which they read.
|
|
113
|
+
*/
|
|
114
|
+
export declare function buildLocalEgressSimulatorEnvironment(port: number): Record<string, string>;
|
|
115
|
+
/**
|
|
116
|
+
* Set the local egress proxy environment in a booted simulator when a local
|
|
117
|
+
* egress session is active. Returns false when local egress is not configured
|
|
118
|
+
* or the environment could not be set. Failure is a warning, not an error: the
|
|
119
|
+
* session still works, but clients that read these variables then exit from
|
|
120
|
+
* this worker instead of the egress client, which the warning spells out.
|
|
121
|
+
*/
|
|
122
|
+
export declare function configureSimulatorProxyEnvironmentAsync({ udid, env, logger, handoffPath, }: {
|
|
123
|
+
udid: IosSimulatorUuid;
|
|
124
|
+
env: NodeJS.ProcessEnv;
|
|
125
|
+
logger: bunyan;
|
|
126
|
+
handoffPath?: string;
|
|
127
|
+
}): Promise<boolean>;
|
|
104
128
|
export declare function writeLocalEgressHandoffAsync(handoff: LocalEgressHandoff, handoffPath?: string): Promise<void>;
|
|
105
129
|
export declare function readLocalEgressHandoffAsync(handoffPath?: string): Promise<LocalEgressHandoff | null>;
|
|
106
130
|
/** remoteConfig fields the CLI needs to start the egress client. */
|
|
@@ -3,7 +3,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
3
3
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
4
|
};
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
-
exports.CHISEL_VERSION = exports.LOCAL_EGRESS_HANDOFF_PATH = exports.LOCAL_EGRESS_USERNAME = exports.LOCAL_EGRESS_PROXY_PORT = exports.LOCAL_EGRESS_PROXY_HOST = void 0;
|
|
6
|
+
exports.LOCAL_EGRESS_NO_PROXY = exports.CHISEL_VERSION = exports.LOCAL_EGRESS_HANDOFF_PATH = exports.LOCAL_EGRESS_USERNAME = exports.LOCAL_EGRESS_PROXY_PORT = exports.LOCAL_EGRESS_PROXY_HOST = void 0;
|
|
7
7
|
exports.getChiselAssetName = getChiselAssetName;
|
|
8
8
|
exports.getChiselDownloadUrl = getChiselDownloadUrl;
|
|
9
9
|
exports.downloadChiselAsync = downloadChiselAsync;
|
|
@@ -16,6 +16,8 @@ exports.parseNetworkServiceNameForDevice = parseNetworkServiceNameForDevice;
|
|
|
16
16
|
exports.resolveActiveNetworkServiceNameAsync = resolveActiveNetworkServiceNameAsync;
|
|
17
17
|
exports.buildNetworksetupProxyArgs = buildNetworksetupProxyArgs;
|
|
18
18
|
exports.configureSystemProxyAsync = configureSystemProxyAsync;
|
|
19
|
+
exports.buildLocalEgressSimulatorEnvironment = buildLocalEgressSimulatorEnvironment;
|
|
20
|
+
exports.configureSimulatorProxyEnvironmentAsync = configureSimulatorProxyEnvironmentAsync;
|
|
19
21
|
exports.writeLocalEgressHandoffAsync = writeLocalEgressHandoffAsync;
|
|
20
22
|
exports.readLocalEgressHandoffAsync = readLocalEgressHandoffAsync;
|
|
21
23
|
exports.buildEgressRemoteConfigFields = buildEgressRemoteConfigFields;
|
|
@@ -37,6 +39,7 @@ const node_os_1 = __importDefault(require("node:os"));
|
|
|
37
39
|
const node_path_1 = __importDefault(require("node:path"));
|
|
38
40
|
const promises_1 = require("node:stream/promises");
|
|
39
41
|
const node_zlib_1 = __importDefault(require("node:zlib"));
|
|
42
|
+
const IosSimulatorUtils_1 = require("../../utils/IosSimulatorUtils");
|
|
40
43
|
const retry_1 = require("../../utils/retry");
|
|
41
44
|
const remoteDeviceRunSession_1 = require("./remoteDeviceRunSession");
|
|
42
45
|
/**
|
|
@@ -253,6 +256,61 @@ async function configureSystemProxyAsync({ env, logger, port, signal, }) {
|
|
|
253
256
|
logger.info(`System proxy for "${service}" set to ${exports.LOCAL_EGRESS_PROXY_HOST}:${port}.`);
|
|
254
257
|
return { service };
|
|
255
258
|
}
|
|
259
|
+
exports.LOCAL_EGRESS_NO_PROXY = 'localhost,127.0.0.1,::1';
|
|
260
|
+
/**
|
|
261
|
+
* Proxy environment for processes inside the simulator. CFNetwork ignores these
|
|
262
|
+
* variables and follows the system proxy, but clients with their own network
|
|
263
|
+
* stack that read them (gRPC and therefore Firestore, libcurl, Go) then send
|
|
264
|
+
* HTTP CONNECT requests to the same loopback port. Loopback is excluded so the
|
|
265
|
+
* ports forwarded for `--egress-allow` behave the same on both paths. Lowercase
|
|
266
|
+
* and uppercase forms are both set because clients differ in which they read.
|
|
267
|
+
*/
|
|
268
|
+
function buildLocalEgressSimulatorEnvironment(port) {
|
|
269
|
+
const proxyUrl = `http://${exports.LOCAL_EGRESS_PROXY_HOST}:${port}`;
|
|
270
|
+
return {
|
|
271
|
+
http_proxy: proxyUrl,
|
|
272
|
+
https_proxy: proxyUrl,
|
|
273
|
+
HTTP_PROXY: proxyUrl,
|
|
274
|
+
HTTPS_PROXY: proxyUrl,
|
|
275
|
+
grpc_proxy: proxyUrl,
|
|
276
|
+
no_proxy: exports.LOCAL_EGRESS_NO_PROXY,
|
|
277
|
+
NO_PROXY: exports.LOCAL_EGRESS_NO_PROXY,
|
|
278
|
+
};
|
|
279
|
+
}
|
|
280
|
+
/**
|
|
281
|
+
* Set the local egress proxy environment in a booted simulator when a local
|
|
282
|
+
* egress session is active. Returns false when local egress is not configured
|
|
283
|
+
* or the environment could not be set. Failure is a warning, not an error: the
|
|
284
|
+
* session still works, but clients that read these variables then exit from
|
|
285
|
+
* this worker instead of the egress client, which the warning spells out.
|
|
286
|
+
*/
|
|
287
|
+
async function configureSimulatorProxyEnvironmentAsync({ udid, env, logger, handoffPath = exports.LOCAL_EGRESS_HANDOFF_PATH, }) {
|
|
288
|
+
let handoff;
|
|
289
|
+
try {
|
|
290
|
+
handoff = await readLocalEgressHandoffAsync(handoffPath);
|
|
291
|
+
}
|
|
292
|
+
catch (err) {
|
|
293
|
+
logger.warn({ err }, 'Local egress: could not read the local egress handoff, so proxy environment variables were ' +
|
|
294
|
+
'not set in the Simulator. Clients that read http_proxy/https_proxy (for example gRPC and ' +
|
|
295
|
+
'libcurl) will bypass local egress and exit from this worker.');
|
|
296
|
+
return false;
|
|
297
|
+
}
|
|
298
|
+
if (!handoff) {
|
|
299
|
+
return false;
|
|
300
|
+
}
|
|
301
|
+
const variables = buildLocalEgressSimulatorEnvironment(handoff.port);
|
|
302
|
+
try {
|
|
303
|
+
await IosSimulatorUtils_1.IosSimulatorUtils.setLaunchdEnvironmentAsync({ udid, env, variables });
|
|
304
|
+
}
|
|
305
|
+
catch (err) {
|
|
306
|
+
logger.warn({ err }, 'Local egress: could not set proxy environment variables in the Simulator. Clients that read ' +
|
|
307
|
+
'http_proxy/https_proxy (for example gRPC and libcurl) will bypass local egress and exit from ' +
|
|
308
|
+
'this worker. CFNetwork clients (WebKit, URLSession) still follow the system proxy.');
|
|
309
|
+
return false;
|
|
310
|
+
}
|
|
311
|
+
logger.info(`Local egress: proxy environment variables set in the Simulator (${Object.keys(variables).join(', ')}), so clients that read them, such as gRPC and libcurl, also exit from the egress client.`);
|
|
312
|
+
return true;
|
|
313
|
+
}
|
|
256
314
|
async function writeLocalEgressHandoffAsync(handoff, handoffPath = exports.LOCAL_EGRESS_HANDOFF_PATH) {
|
|
257
315
|
await node_fs_1.default.promises.writeFile(handoffPath, JSON.stringify(handoff), {
|
|
258
316
|
encoding: 'utf8',
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
import { type bunyan } from '@expo/logger';
|
|
2
|
+
import { type IosSimulatorUuid } from '../../utils/IosSimulatorUtils';
|
|
3
|
+
/**
|
|
4
|
+
* Worker side of the local egress guard: a dylib injected into every process
|
|
5
|
+
* the simulator launches, which refuses connections that do not go to
|
|
6
|
+
* loopback (where the proxy and the `--egress-allow` forwards live) and
|
|
7
|
+
* records one event per destination per process. This module installs it
|
|
8
|
+
* through the simulator's launchd environment and relays its events into the
|
|
9
|
+
* session log. See resources/egress-guard/README.md.
|
|
10
|
+
*/
|
|
11
|
+
export declare const EGRESS_GUARD_LIBRARY_FILE = "egress-guard.dylib";
|
|
12
|
+
export declare const EGRESS_GUARD_CHECK_FILE = "egress-guard-check";
|
|
13
|
+
export declare const EGRESS_GUARD_LOG_ENV = "EAS_EGRESS_GUARD_LOG";
|
|
14
|
+
export declare const EGRESS_GUARD_MODE_ENV = "EAS_EGRESS_GUARD_MODE";
|
|
15
|
+
export declare const LOCAL_EGRESS_GUARD_LOG_PATH: string;
|
|
16
|
+
export type EgressGuardMode = 'block' | 'log';
|
|
17
|
+
export declare function buildGuardLaunchdEnvironment({ libraryPath, logPath, mode, }: {
|
|
18
|
+
libraryPath: string;
|
|
19
|
+
logPath: string;
|
|
20
|
+
mode: EgressGuardMode;
|
|
21
|
+
}): Record<string, string>;
|
|
22
|
+
export type GuardEvent = {
|
|
23
|
+
process: string;
|
|
24
|
+
pid: number;
|
|
25
|
+
function: string;
|
|
26
|
+
action: 'blocked' | 'logged';
|
|
27
|
+
peer: string;
|
|
28
|
+
/** Image names above the interposer, innermost first. */
|
|
29
|
+
callers: string[];
|
|
30
|
+
};
|
|
31
|
+
/** One tab-separated line written by the guard; see policy.h for the format. */
|
|
32
|
+
export declare function parseGuardLogLine(line: string): GuardEvent | null;
|
|
33
|
+
/** The packaged library, next to the compiled package like record-sim. */
|
|
34
|
+
export declare function resolveEgressGuardLibraryAsync(binDir?: string): Promise<string | null>;
|
|
35
|
+
/** The packaged self-check binary, built alongside the library. */
|
|
36
|
+
export declare function resolveEgressGuardCheckAsync(binDir?: string): Promise<string | null>;
|
|
37
|
+
/**
|
|
38
|
+
* Turns guard events into session log lines: one line the first time a
|
|
39
|
+
* process reaches a destination through a given call, counts after that, and
|
|
40
|
+
* a summary at the end.
|
|
41
|
+
*/
|
|
42
|
+
export declare class GuardEventRelay {
|
|
43
|
+
private readonly limit;
|
|
44
|
+
private readonly seen;
|
|
45
|
+
private readonly peers;
|
|
46
|
+
private readonly processes;
|
|
47
|
+
private blocked;
|
|
48
|
+
private logged;
|
|
49
|
+
private suppressed;
|
|
50
|
+
private logger;
|
|
51
|
+
constructor(logger: bunyan, limit?: number);
|
|
52
|
+
/**
|
|
53
|
+
* Log through a different step's logger from now on. Refusals happen for
|
|
54
|
+
* the life of the session, so they belong to the step that runs the
|
|
55
|
+
* session, not the one that booted the simulator minutes earlier.
|
|
56
|
+
*/
|
|
57
|
+
setLogger(logger: bunyan): void;
|
|
58
|
+
handle(event: GuardEvent): void;
|
|
59
|
+
summary(): {
|
|
60
|
+
blocked: number;
|
|
61
|
+
logged: number;
|
|
62
|
+
distinct: number;
|
|
63
|
+
suppressed: number;
|
|
64
|
+
};
|
|
65
|
+
logSummary(): void;
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Polls a file the simulator processes append to and delivers whole lines.
|
|
69
|
+
* Tolerates the file not existing yet, partial trailing lines, and truncation.
|
|
70
|
+
*/
|
|
71
|
+
export declare class GuardLogTailer {
|
|
72
|
+
private readonly path;
|
|
73
|
+
private readonly onLine;
|
|
74
|
+
private readonly intervalMs;
|
|
75
|
+
private offset;
|
|
76
|
+
private partial;
|
|
77
|
+
private timer;
|
|
78
|
+
private reading;
|
|
79
|
+
constructor({ path: filePath, onLine, intervalMs, }: {
|
|
80
|
+
path: string;
|
|
81
|
+
onLine: (line: string) => void;
|
|
82
|
+
intervalMs?: number;
|
|
83
|
+
});
|
|
84
|
+
start(): void;
|
|
85
|
+
stopAsync(): Promise<void>;
|
|
86
|
+
private readAsync;
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* The environment the simulator's launchd must have from its first process:
|
|
90
|
+
* the guard and the proxy variables. Pass it to `IosSimulatorUtils.bootAsync`,
|
|
91
|
+
* which hands it to launchd before anything is spawned; `launchctl setenv`
|
|
92
|
+
* after boot only reaches later processes. Returns null when no local egress
|
|
93
|
+
* session is active. Throws when the guard library is not packaged, since a
|
|
94
|
+
* local egress session without it would silently leak.
|
|
95
|
+
*/
|
|
96
|
+
export declare function resolveLocalEgressBootEnvironmentAsync({ handoffPath, libraryPath, logPath, mode, }?: {
|
|
97
|
+
handoffPath?: string;
|
|
98
|
+
/** Explicit library path, `null` for "not available"; resolved from the package when omitted. */
|
|
99
|
+
libraryPath?: string | null;
|
|
100
|
+
logPath?: string;
|
|
101
|
+
mode?: EgressGuardMode;
|
|
102
|
+
}): Promise<Record<string, string> | null>;
|
|
103
|
+
/**
|
|
104
|
+
* Install the guard into a simulator when a local egress session is active,
|
|
105
|
+
* and start relaying its events into the session log. Returns false when
|
|
106
|
+
* there is no local egress session. Throws when the library is not packaged
|
|
107
|
+
* or launchd could not be configured: a local egress session without the
|
|
108
|
+
* guard would silently leak, so it must not start. Only an unwritable event
|
|
109
|
+
* log is a warning, since refusals still happen and only reporting is lost.
|
|
110
|
+
*
|
|
111
|
+
* Call this as soon as `simctl boot` returns: launchd is up and nothing else
|
|
112
|
+
* has started, so every process the boot spawns inherits the guard. Verify
|
|
113
|
+
* with `verifyLocalEgressGuardAsync` once boot completes.
|
|
114
|
+
*/
|
|
115
|
+
export declare function installLocalEgressGuardAsync({ udid, env, logger, handoffPath, libraryPath, logPath, mode, tailIntervalMs, }: {
|
|
116
|
+
udid: IosSimulatorUuid;
|
|
117
|
+
env: NodeJS.ProcessEnv;
|
|
118
|
+
logger: bunyan;
|
|
119
|
+
handoffPath?: string;
|
|
120
|
+
/** Explicit library path, `null` for "not available"; resolved from the package when omitted. */
|
|
121
|
+
libraryPath?: string | null;
|
|
122
|
+
logPath?: string;
|
|
123
|
+
mode?: EgressGuardMode;
|
|
124
|
+
tailIntervalMs?: number;
|
|
125
|
+
}): Promise<boolean>;
|
|
126
|
+
export type GuardCoverage = {
|
|
127
|
+
/** Simulator processes with the guard library mapped. */
|
|
128
|
+
covered: string[];
|
|
129
|
+
/** Simulator processes without it: started before the guard was installed. */
|
|
130
|
+
uncovered: string[];
|
|
131
|
+
/** Pids behind `uncovered`, for comparing samples. */
|
|
132
|
+
uncoveredPids: number[];
|
|
133
|
+
};
|
|
134
|
+
/**
|
|
135
|
+
* Which simulator processes have the guard library mapped, from
|
|
136
|
+
* `ps -axo pid=,ppid=,comm=` and `lsof -nP -a -p <pids> -d txt -F pn` output.
|
|
137
|
+
* A process is covered when any of its mapped images is the guard library.
|
|
138
|
+
*/
|
|
139
|
+
export declare function parseGuardCoverage(psOutput: string, lsofOutput: string): GuardCoverage;
|
|
140
|
+
/**
|
|
141
|
+
* A process caught between fork and exec has nothing mapped yet and looks
|
|
142
|
+
* uncovered for an instant. Two samples a moment apart separate those from
|
|
143
|
+
* processes that really run without the guard: only pids uncovered in both
|
|
144
|
+
* count, reported with the later sample's names.
|
|
145
|
+
*/
|
|
146
|
+
export declare function mergeGuardCoverageSamples(first: GuardCoverage, second: GuardCoverage): GuardCoverage;
|
|
147
|
+
/**
|
|
148
|
+
* Measure and log guard coverage across the simulator's processes. Observation
|
|
149
|
+
* only: the uncovered set is whatever started before the guard was installed,
|
|
150
|
+
* which is nothing when installation runs right after `simctl boot`.
|
|
151
|
+
*/
|
|
152
|
+
export declare function reportLocalEgressGuardCoverageAsync({ env, logger, sampleIntervalMs, }: {
|
|
153
|
+
env: NodeJS.ProcessEnv;
|
|
154
|
+
logger: bunyan;
|
|
155
|
+
sampleIntervalMs?: number;
|
|
156
|
+
}): Promise<GuardCoverage | null>;
|
|
157
|
+
/**
|
|
158
|
+
* Run the packaged self-check inside the simulator: it must find the guard
|
|
159
|
+
* loaded in a fresh process and see it behave as `mode` says. Throws when the
|
|
160
|
+
* check binary is missing or the check fails, which fails the session.
|
|
161
|
+
*/
|
|
162
|
+
export declare function verifyLocalEgressGuardAsync({ udid, env, logger, mode, checkPath, }: {
|
|
163
|
+
udid: IosSimulatorUuid;
|
|
164
|
+
env: NodeJS.ProcessEnv;
|
|
165
|
+
logger: bunyan;
|
|
166
|
+
mode?: EgressGuardMode;
|
|
167
|
+
/** Explicit check binary path, `null` for "not available"; resolved from the package when omitted. */
|
|
168
|
+
checkPath?: string | null;
|
|
169
|
+
}): Promise<void>;
|
|
170
|
+
/**
|
|
171
|
+
* Attribute guard events to the step that runs the session from now on. The
|
|
172
|
+
* relay starts under the simulator boot step's logger; once the session step
|
|
173
|
+
* takes over, its lines should appear under that step in the job log, next
|
|
174
|
+
* to the rest of the session's output, rather than under a step that already
|
|
175
|
+
* finished.
|
|
176
|
+
*/
|
|
177
|
+
export declare function rebindLocalEgressGuardRelays(logger: bunyan): void;
|
|
178
|
+
/** Stop relaying and write each relay's summary; called from the session cleanup. */
|
|
179
|
+
export declare function stopLocalEgressGuardRelaysAsync(logger: bunyan): Promise<void>;
|