@efffrida/frida-tools 0.0.14 → 0.0.16

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.
Files changed (92) hide show
  1. package/README.md +6 -10
  2. package/dist/{dts/FridaDevice.d.ts → FridaDevice.d.ts} +48 -4
  3. package/dist/FridaDevice.d.ts.map +1 -0
  4. package/dist/{esm/FridaDevice.js → FridaDevice.js} +22 -2
  5. package/dist/FridaDevice.js.map +1 -0
  6. package/dist/{dts/FridaDeviceAcquisitionError.d.ts → FridaDeviceAcquisitionError.d.ts} +2 -1
  7. package/dist/FridaDeviceAcquisitionError.d.ts.map +1 -0
  8. package/dist/{esm/FridaDeviceAcquisitionError.js → FridaDeviceAcquisitionError.js} +5 -1
  9. package/dist/FridaDeviceAcquisitionError.js.map +1 -0
  10. package/dist/FridaScript.d.ts +104 -0
  11. package/dist/FridaScript.d.ts.map +1 -0
  12. package/dist/{esm/FridaScript.js → FridaScript.js} +5 -0
  13. package/dist/FridaScript.js.map +1 -0
  14. package/dist/FridaSession.d.ts +72 -0
  15. package/dist/FridaSession.d.ts.map +1 -0
  16. package/dist/{esm/FridaSession.js → FridaSession.js} +10 -0
  17. package/dist/FridaSession.js.map +1 -0
  18. package/dist/FridaSessionError.d.ts.map +1 -0
  19. package/dist/FridaSessionError.js.map +1 -0
  20. package/dist/{dts/index.d.ts → index.d.ts} +8 -5
  21. package/dist/index.d.ts.map +1 -0
  22. package/dist/{esm/index.js → index.js} +3 -0
  23. package/dist/index.js.map +1 -0
  24. package/dist/{dts/internal → internal}/device.d.ts.map +1 -1
  25. package/dist/internal/device.js +189 -0
  26. package/dist/internal/device.js.map +1 -0
  27. package/dist/{dts/internal → internal}/script.d.ts.map +1 -1
  28. package/dist/internal/script.js +229 -0
  29. package/dist/internal/script.js.map +1 -0
  30. package/dist/{dts/internal → internal}/session.d.ts.map +1 -1
  31. package/dist/internal/session.js +124 -0
  32. package/dist/internal/session.js.map +1 -0
  33. package/package.json +54 -68
  34. package/src/FridaDevice.ts +92 -8
  35. package/src/FridaDeviceAcquisitionError.ts +6 -2
  36. package/src/FridaScript.ts +67 -31
  37. package/src/FridaSession.ts +31 -5
  38. package/src/FridaSessionError.ts +1 -1
  39. package/src/index.ts +9 -5
  40. package/src/internal/device.ts +311 -32
  41. package/src/internal/script.ts +286 -118
  42. package/src/internal/session.ts +136 -27
  43. package/FridaDevice/package.json +0 -6
  44. package/FridaDeviceAcquisitionError/package.json +0 -6
  45. package/FridaScript/package.json +0 -6
  46. package/FridaSession/package.json +0 -6
  47. package/FridaSessionError/package.json +0 -6
  48. package/dist/cjs/FridaDevice.js +0 -60
  49. package/dist/cjs/FridaDevice.js.map +0 -1
  50. package/dist/cjs/FridaDeviceAcquisitionError.js +0 -33
  51. package/dist/cjs/FridaDeviceAcquisitionError.js.map +0 -1
  52. package/dist/cjs/FridaScript.js +0 -40
  53. package/dist/cjs/FridaScript.js.map +0 -1
  54. package/dist/cjs/FridaSession.js +0 -45
  55. package/dist/cjs/FridaSession.js.map +0 -1
  56. package/dist/cjs/FridaSessionError.js +0 -37
  57. package/dist/cjs/FridaSessionError.js.map +0 -1
  58. package/dist/cjs/index.js +0 -18
  59. package/dist/cjs/index.js.map +0 -1
  60. package/dist/cjs/internal/device.js +0 -68
  61. package/dist/cjs/internal/device.js.map +0 -1
  62. package/dist/cjs/internal/script.js +0 -163
  63. package/dist/cjs/internal/script.js.map +0 -1
  64. package/dist/cjs/internal/session.js +0 -55
  65. package/dist/cjs/internal/session.js.map +0 -1
  66. package/dist/dts/FridaDevice.d.ts.map +0 -1
  67. package/dist/dts/FridaDeviceAcquisitionError.d.ts.map +0 -1
  68. package/dist/dts/FridaScript.d.ts +0 -80
  69. package/dist/dts/FridaScript.d.ts.map +0 -1
  70. package/dist/dts/FridaSession.d.ts +0 -56
  71. package/dist/dts/FridaSession.d.ts.map +0 -1
  72. package/dist/dts/FridaSessionError.d.ts.map +0 -1
  73. package/dist/dts/index.d.ts.map +0 -1
  74. package/dist/esm/FridaDevice.js.map +0 -1
  75. package/dist/esm/FridaDeviceAcquisitionError.js.map +0 -1
  76. package/dist/esm/FridaScript.js.map +0 -1
  77. package/dist/esm/FridaSession.js.map +0 -1
  78. package/dist/esm/FridaSessionError.js.map +0 -1
  79. package/dist/esm/index.js.map +0 -1
  80. package/dist/esm/internal/device.js +0 -55
  81. package/dist/esm/internal/device.js.map +0 -1
  82. package/dist/esm/internal/script.js +0 -155
  83. package/dist/esm/internal/script.js.map +0 -1
  84. package/dist/esm/internal/session.js +0 -44
  85. package/dist/esm/internal/session.js.map +0 -1
  86. package/dist/esm/package.json +0 -4
  87. package/index/package.json +0 -6
  88. /package/dist/{dts/FridaSessionError.d.ts → FridaSessionError.d.ts} +0 -0
  89. /package/dist/{esm/FridaSessionError.js → FridaSessionError.js} +0 -0
  90. /package/dist/{dts/internal → internal}/device.d.ts +0 -0
  91. /package/dist/{dts/internal → internal}/script.d.ts +0 -0
  92. /package/dist/{dts/internal → internal}/session.d.ts +0 -0
@@ -1,11 +1,26 @@
1
- import type * as FridaDevice from "../FridaDevice.js";
1
+ import type * as CommandExecutor from "@effect/platform/CommandExecutor";
2
+ import type * as ConfigError from "effect/ConfigError";
3
+ import type * as Scope from "effect/Scope";
4
+ import type * as FridaDevice from "../FridaDevice.ts";
2
5
 
6
+ import * as Command from "@effect/platform/Command";
7
+ import * as PlatformError from "@effect/platform/Error";
8
+ import * as Chunk from "effect/Chunk";
9
+ import * as Config from "effect/Config";
3
10
  import * as Context from "effect/Context";
4
11
  import * as Effect from "effect/Effect";
5
12
  import * as Layer from "effect/Layer";
13
+ import * as ParseResult from "effect/ParseResult";
6
14
  import * as Predicate from "effect/Predicate";
15
+ import * as Schema from "effect/Schema";
16
+ import * as Stream from "effect/Stream";
17
+ import * as String from "effect/String";
18
+ import * as Tuple from "effect/Tuple";
7
19
  import * as Frida from "frida";
8
- import * as FridaDeviceAcquisitionError from "../FridaDeviceAcquisitionError.js";
20
+ import * as net from "node:net";
21
+ import * as path from "node:path";
22
+
23
+ import * as FridaDeviceAcquisitionError from "../FridaDeviceAcquisitionError.ts";
9
24
 
10
25
  /** @internal */
11
26
  export const FridaDeviceTypeId: FridaDevice.FridaDeviceTypeId = Symbol.for(
@@ -19,65 +34,302 @@ export const Tag = Context.GenericTag<FridaDevice.FridaDevice>("@efffrida/frida-
19
34
  export const isFridaDevice = (u: unknown): u is FridaDevice.FridaDevice => Predicate.hasProperty(u, FridaDeviceTypeId);
20
35
 
21
36
  /** @internal */
22
- export const acquireUsbDevice = (
23
- options?: Frida.GetDeviceOptions | undefined
24
- ): Effect.Effect<FridaDevice.FridaDevice, FridaDeviceAcquisitionError.FridaDeviceAcquisitionError, never> =>
37
+ export const acquireLocalDevice = (): Effect.Effect<
38
+ FridaDevice.FridaDevice,
39
+ FridaDeviceAcquisitionError.FridaDeviceAcquisitionError,
40
+ never
41
+ > =>
25
42
  Effect.map(
26
43
  Effect.tryPromise({
27
- try: () => Frida.getUsbDevice(options),
44
+ try: (signal) => {
45
+ const cancellable = new Frida.Cancellable();
46
+ signal.onabort = () => cancellable.cancel();
47
+ return Frida.getLocalDevice(cancellable);
48
+ },
28
49
  catch: (cause) =>
29
50
  new FridaDeviceAcquisitionError.FridaDeviceAcquisitionError({
30
51
  cause,
31
52
  attempts: 1,
32
- acquisitionMethod: "usb",
53
+ acquisitionMethod: "local",
33
54
  }),
34
55
  }),
35
- (device) => ({ device, [FridaDeviceTypeId]: FridaDeviceTypeId }) as const
56
+ (device) =>
57
+ ({
58
+ device,
59
+ host: "local://",
60
+ [FridaDeviceTypeId]: FridaDeviceTypeId,
61
+ }) as const
36
62
  );
37
63
 
38
64
  /** @internal */
39
- export const acquireRemoteDevice = (
40
- address: string,
41
- options?: Frida.RemoteDeviceOptions | undefined
65
+ export const acquireUsbDevice = (
66
+ options?: Frida.GetDeviceOptions | undefined
42
67
  ): Effect.Effect<FridaDevice.FridaDevice, FridaDeviceAcquisitionError.FridaDeviceAcquisitionError, never> =>
43
68
  Effect.map(
44
69
  Effect.tryPromise({
45
- try: () => Frida.getDeviceManager().addRemoteDevice(address, options),
70
+ try: (signal) => {
71
+ const cancellable = new Frida.Cancellable();
72
+ signal.onabort = () => cancellable.cancel();
73
+ return Frida.getUsbDevice(options, cancellable);
74
+ },
46
75
  catch: (cause) =>
47
76
  new FridaDeviceAcquisitionError.FridaDeviceAcquisitionError({
48
77
  cause,
49
78
  attempts: 1,
50
- acquisitionMethod: "remote",
79
+ acquisitionMethod: "usb",
51
80
  }),
52
81
  }),
53
- (device) => ({ device, [FridaDeviceTypeId]: FridaDeviceTypeId }) as const
82
+ (device) =>
83
+ ({
84
+ device,
85
+ host: "usb://",
86
+ [FridaDeviceTypeId]: FridaDeviceTypeId,
87
+ }) as const
54
88
  );
55
89
 
56
90
  /** @internal */
57
- export const acquireLocalDevice = (): Effect.Effect<
91
+ export const acquireRemoteDevice = (
92
+ address: string,
93
+ options?: Frida.RemoteDeviceOptions | undefined
94
+ ): Effect.Effect<FridaDevice.FridaDevice, FridaDeviceAcquisitionError.FridaDeviceAcquisitionError, Scope.Scope> => {
95
+ const acquire = Effect.tryPromise({
96
+ try: (signal) => {
97
+ const cancellable = new Frida.Cancellable();
98
+ signal.onabort = () => cancellable.cancel();
99
+ return Frida.getDeviceManager().addRemoteDevice(address, options, cancellable);
100
+ },
101
+ catch: (cause) =>
102
+ new FridaDeviceAcquisitionError.FridaDeviceAcquisitionError({
103
+ cause,
104
+ attempts: 1,
105
+ acquisitionMethod: "remote",
106
+ }),
107
+ });
108
+
109
+ const release = (_device: Frida.Device) =>
110
+ Effect.promise((signal) => {
111
+ const cancellable = new Frida.Cancellable();
112
+ signal.onabort = () => cancellable.cancel();
113
+ return Frida.getDeviceManager().removeRemoteDevice(address, cancellable);
114
+ });
115
+
116
+ const resource = Effect.acquireRelease(acquire, release);
117
+
118
+ return Effect.map(
119
+ resource,
120
+ (device) =>
121
+ ({
122
+ device,
123
+ host: `remote://${address}`,
124
+ [FridaDeviceTypeId]: FridaDeviceTypeId,
125
+ }) as const
126
+ );
127
+ };
128
+
129
+ /** @internal */
130
+ export const acquireAndroidEmulatorDevice = Effect.fn("acquireAndroidEmulatorDevice")(
131
+ function* (
132
+ name: string,
133
+ options?:
134
+ | {
135
+ hidden?: boolean | undefined;
136
+ adbExecutable?: string | undefined;
137
+ fridaExecutable?: string | undefined;
138
+ emulatorExecutable?: string | undefined;
139
+ extraEmulatorArgs?: Array<string> | undefined;
140
+ }
141
+ | undefined
142
+ ) {
143
+ const hidden = options?.hidden ?? false;
144
+ const adbExecutable = options?.adbExecutable ?? "adb";
145
+ const emulatorExecutable = options?.emulatorExecutable ?? "emulator";
146
+ const fridaExecutable = options?.fridaExecutable ?? "/data/local/tmp/frida-server";
147
+
148
+ yield* Effect.annotateCurrentSpan({
149
+ "emulator.name": name,
150
+ "emulator.hidden": hidden,
151
+ "adb.path": adbExecutable,
152
+ "frida.path": fridaExecutable,
153
+ "emulator.path": emulatorExecutable,
154
+ });
155
+
156
+ const getTwoConsecutiveFreePorts = (startingAt: number = 2000) =>
157
+ Effect.async<readonly [firstPort: number, secondPort: number], never, never>((resume) => {
158
+ const socket1 = net.createConnection(startingAt);
159
+ const socket2 = net.createConnection(startingAt + 1);
160
+
161
+ const cleanup = () => {
162
+ socket1.removeAllListeners();
163
+ socket2.removeAllListeners();
164
+ socket1.destroy();
165
+ socket2.destroy();
166
+ };
167
+
168
+ const onError = (error: NodeJS.ErrnoException) => {
169
+ if (error.code !== "ECONNREFUSED") {
170
+ resume(Effect.dieMessage("Failed to get two free consecutive ports"));
171
+ }
172
+
173
+ if (socket1.connecting === false && socket2.connecting === false) {
174
+ cleanup();
175
+ resume(Effect.succeed(Tuple.make(startingAt, startingAt + 1)));
176
+ }
177
+ };
178
+
179
+ const onConnect = () => {
180
+ cleanup();
181
+ resume(getTwoConsecutiveFreePorts(startingAt + 1));
182
+ };
183
+
184
+ socket1.once("error", onError);
185
+ socket2.once("error", onError);
186
+ socket1.once("connect", onConnect);
187
+ socket2.once("connect", onConnect);
188
+ return Effect.sync(() => cleanup());
189
+ });
190
+
191
+ const filterExitOk = (code: number) => (code === 0 ? Effect.void : Effect.fail(code));
192
+
193
+ const [firstPort, _secondPort] = yield* getTwoConsecutiveFreePorts();
194
+ const emulator = `emulator-${firstPort}`;
195
+
196
+ const emulatorProcess = yield* Command.make(
197
+ emulatorExecutable,
198
+ `@${name}`,
199
+ "-delay-adb",
200
+ "-read-only",
201
+ "-no-snapshot-save",
202
+ "-port",
203
+ firstPort.toString(),
204
+ ...(hidden ? ["-no-window"] : []),
205
+ ...(options?.extraEmulatorArgs ?? [])
206
+ ).pipe(Command.start);
207
+
208
+ yield* Effect.addFinalizer(() =>
209
+ Command.make(adbExecutable, "-s", emulator, "emu", "kill")
210
+ .pipe(Command.exitCode)
211
+ .pipe(Effect.flatMap(filterExitOk))
212
+ .pipe(Effect.orDie)
213
+ );
214
+
215
+ const isBootCompletedPredicate = Predicate.or(
216
+ String.includes("Successfully loaded"),
217
+ String.includes("Boot completed")
218
+ );
219
+
220
+ const decoder = new TextDecoder();
221
+ yield* emulatorProcess.stderr.pipe(Stream.runHead).pipe(Effect.forkScoped);
222
+ yield* emulatorProcess.stdout
223
+ .pipe(Stream.mapChunks(Chunk.map((bytes) => decoder.decode(bytes))))
224
+ .pipe(Stream.splitLines)
225
+ .pipe(Stream.takeUntil(isBootCompletedPredicate))
226
+ .pipe(Stream.runDrain);
227
+
228
+ yield* Command.make(adbExecutable, `-s`, emulator, "wait-for-device")
229
+ .pipe(Command.exitCode)
230
+ .pipe(Effect.flatMap(filterExitOk))
231
+ .pipe(
232
+ Effect.mapError(
233
+ (code) =>
234
+ new FridaDeviceAcquisitionError.FridaDeviceAcquisitionError({
235
+ attempts: 1,
236
+ acquisitionMethod: "android-emulator",
237
+ cause: new Error(`ADB wait-for-device failed with exit code ${code}`),
238
+ })
239
+ )
240
+ );
241
+
242
+ yield* Command.make(adbExecutable, `-s`, emulator, "root")
243
+ .pipe(Command.exitCode)
244
+ .pipe(Effect.flatMap(filterExitOk))
245
+ .pipe(
246
+ Effect.mapError(
247
+ (code) =>
248
+ new FridaDeviceAcquisitionError.FridaDeviceAcquisitionError({
249
+ attempts: 1,
250
+ acquisitionMethod: "android-emulator",
251
+ cause: new Error(`ADB root failed with exit code ${code}`),
252
+ })
253
+ )
254
+ );
255
+
256
+ yield* Command.make(adbExecutable, "-s", emulator, "shell", `"${fridaExecutable}"`)
257
+ .pipe(Command.start)
258
+ .pipe(Effect.andThen(Effect.sleep("3 seconds")));
259
+
260
+ const fridaPort = yield* Command.make(adbExecutable, "-s", emulator, "forward", "tcp:0", "tcp:27042")
261
+ .pipe(Command.string)
262
+ .pipe(Effect.flatMap(Schema.decode(Schema.NumberFromString)));
263
+
264
+ const { host: _host, ...remoteDevice } = yield* acquireRemoteDevice(`localhost:${fridaPort}`);
265
+
266
+ return {
267
+ ...remoteDevice,
268
+ host: `android-emulator://${emulator}`,
269
+ } as const;
270
+ },
271
+ Effect.timeoutFail({
272
+ duration: "1 minute",
273
+ onTimeout: () =>
274
+ new FridaDeviceAcquisitionError.FridaDeviceAcquisitionError({
275
+ attempts: 1,
276
+ acquisitionMethod: "android-emulator",
277
+ cause: new Error("Timeout while acquiring Android emulator device"),
278
+ }),
279
+ }),
280
+ Effect.catchIf(
281
+ Predicate.or(PlatformError.isPlatformError, ParseResult.isParseError),
282
+ (cause) =>
283
+ new FridaDeviceAcquisitionError.FridaDeviceAcquisitionError({
284
+ cause,
285
+ attempts: 1,
286
+ acquisitionMethod: "android-emulator",
287
+ })
288
+ )
289
+ );
290
+
291
+ /** @internal */
292
+ export const acquireAndroidEmulatorDeviceConfig = (
293
+ name: string,
294
+ options?:
295
+ | {
296
+ hidden?: boolean | undefined;
297
+ fridaExecutable?: string | undefined;
298
+ extraEmulatorArgs?: Array<string> | undefined;
299
+ }
300
+ | undefined
301
+ ) =>
302
+ Config.string("ANDROID_SDK").pipe(
303
+ Config.map((androidSdk) => ({
304
+ adbExecutable: path.join(androidSdk, "platform-tools", "adb"),
305
+ emulatorExecutable: path.join(androidSdk, "emulator", "emulator"),
306
+ })),
307
+ // TODO: Should this be optional?
308
+ Config.withDefault({
309
+ adbExecutable: "adb",
310
+ emulatorExecutable: "emulator",
311
+ }),
312
+ Effect.flatMap((androidSdk) =>
313
+ acquireAndroidEmulatorDevice(name, {
314
+ ...androidSdk,
315
+ ...options,
316
+ })
317
+ )
318
+ );
319
+
320
+ /** @internal */
321
+ export const layerLocalDevice: Layer.Layer<
58
322
  FridaDevice.FridaDevice,
59
323
  FridaDeviceAcquisitionError.FridaDeviceAcquisitionError,
60
324
  never
61
- > =>
62
- Effect.map(
63
- Effect.tryPromise({
64
- try: () => Frida.getLocalDevice(),
65
- catch: (cause) =>
66
- new FridaDeviceAcquisitionError.FridaDeviceAcquisitionError({
67
- cause,
68
- attempts: 1,
69
- acquisitionMethod: "local",
70
- }),
71
- }),
72
- (device) => ({ device, [FridaDeviceTypeId]: FridaDeviceTypeId }) as const
73
- );
325
+ > = Layer.effect(Tag, acquireLocalDevice());
74
326
 
75
327
  /** @internal */
76
328
  export const layerRemoteDevice = (
77
329
  address: string,
78
330
  options?: Frida.RemoteDeviceOptions | undefined
79
331
  ): Layer.Layer<FridaDevice.FridaDevice, FridaDeviceAcquisitionError.FridaDeviceAcquisitionError, never> =>
80
- Layer.effect(Tag, acquireRemoteDevice(address, options));
332
+ Layer.scoped(Tag, acquireRemoteDevice(address, options));
81
333
 
82
334
  /** @internal */
83
335
  export const layerUsbDevice = (
@@ -86,8 +338,35 @@ export const layerUsbDevice = (
86
338
  Layer.effect(Tag, acquireUsbDevice(options));
87
339
 
88
340
  /** @internal */
89
- export const layerLocalDevice: Layer.Layer<
341
+ export const layerAndroidEmulatorDevice = (
342
+ name: string,
343
+ options?:
344
+ | {
345
+ hidden?: boolean | undefined;
346
+ adbExecutable?: string | undefined;
347
+ fridaExecutable?: string | undefined;
348
+ emulatorExecutable?: string | undefined;
349
+ extraEmulatorArgs?: Array<string> | undefined;
350
+ }
351
+ | undefined
352
+ ): Layer.Layer<
90
353
  FridaDevice.FridaDevice,
91
354
  FridaDeviceAcquisitionError.FridaDeviceAcquisitionError,
92
- never
93
- > = Layer.effect(Tag, acquireLocalDevice());
355
+ CommandExecutor.CommandExecutor
356
+ > => Layer.scoped(Tag, acquireAndroidEmulatorDevice(name, options));
357
+
358
+ /** @internal */
359
+ export const layerAndroidEmulatorDeviceConfig = (
360
+ name: string,
361
+ options?:
362
+ | {
363
+ hidden?: boolean | undefined;
364
+ fridaExecutable?: string | undefined;
365
+ extraEmulatorArgs?: Array<string> | undefined;
366
+ }
367
+ | undefined
368
+ ): Layer.Layer<
369
+ FridaDevice.FridaDevice,
370
+ ConfigError.ConfigError | FridaDeviceAcquisitionError.FridaDeviceAcquisitionError,
371
+ CommandExecutor.CommandExecutor
372
+ > => Layer.scoped(Tag, acquireAndroidEmulatorDeviceConfig(name, options));