@efffrida/rpc 0.0.1

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/package.json ADDED
@@ -0,0 +1,53 @@
1
+ {
2
+ "name": "@efffrida/rpc",
3
+ "version": "0.0.1",
4
+ "description": "effect rpc-frida",
5
+ "license": "GPL-3.0-only",
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "https://github.com/leoconforti/efffrida.git",
9
+ "directory": "packages/rpc"
10
+ },
11
+ "sideEffects": [],
12
+ "author": "Leo Conforti <leo@leoconforti.us> (https://leoconforti.us)",
13
+ "homepage": "https://github.com/leoconforti/efffrida",
14
+ "dependencies": {
15
+ "@efffrida/frida-tools": "^0.0.1"
16
+ },
17
+ "peerDependencies": {
18
+ "@effect/platform": "0.80.8",
19
+ "@effect/rpc": "0.55.10",
20
+ "effect": "3.14.7"
21
+ },
22
+ "main": "./dist/cjs/index.js",
23
+ "module": "./dist/esm/index.js",
24
+ "types": "./dist/dts/index.d.ts",
25
+ "exports": {
26
+ "./package.json": "./package.json",
27
+ ".": {
28
+ "types": "./dist/dts/index.d.ts",
29
+ "import": "./dist/esm/index.js",
30
+ "default": "./dist/cjs/index.js"
31
+ },
32
+ "./FridaRpcClient": {
33
+ "types": "./dist/dts/FridaRpcClient.d.ts",
34
+ "import": "./dist/esm/FridaRpcClient.js",
35
+ "default": "./dist/cjs/FridaRpcClient.js"
36
+ },
37
+ "./FridaRpcServer": {
38
+ "types": "./dist/dts/FridaRpcServer.d.ts",
39
+ "import": "./dist/esm/FridaRpcServer.js",
40
+ "default": "./dist/cjs/FridaRpcServer.js"
41
+ }
42
+ },
43
+ "typesVersions": {
44
+ "*": {
45
+ "FridaRpcClient": [
46
+ "./dist/dts/FridaRpcClient.d.ts"
47
+ ],
48
+ "FridaRpcServer": [
49
+ "./dist/dts/FridaRpcServer.d.ts"
50
+ ]
51
+ }
52
+ }
53
+ }
@@ -0,0 +1,113 @@
1
+ /**
2
+ * Implements a Frida RPC client protocol for effect using the frida script
3
+ * exports. The reason we don't use the send/recv script channels is because
4
+ * those are shared channels by everybody.
5
+ *
6
+ * @since 1.0.0
7
+ */
8
+
9
+ import type * as RpcMessage from "@effect/rpc/RpcMessage";
10
+ import type * as FridaSessionError from "@efffrida/frida-tools/FridaSessionError";
11
+ import type * as Types from "effect/Types";
12
+
13
+ import * as RpcClient from "@effect/rpc/RpcClient";
14
+ import * as RpcSerialization from "@effect/rpc/RpcSerialization";
15
+ import * as FridaScript from "@efffrida/frida-tools/FridaScript";
16
+ import * as Effect from "effect/Effect";
17
+ import * as Function from "effect/Function";
18
+ import * as Layer from "effect/Layer";
19
+ import * as Predicate from "effect/Predicate";
20
+ import * as Schema from "effect/Schema";
21
+ import * as Stream from "effect/Stream";
22
+
23
+ /**
24
+ * @since 1.0.0
25
+ * @category Protocol
26
+ */
27
+ export const makeProtocolFrida = (
28
+ script: FridaScript.FridaScript,
29
+ options?:
30
+ | {
31
+ readonly exportName?: string | undefined;
32
+ readonly rpcIsAvailableWhen?: ((message: string) => boolean) | undefined;
33
+ }
34
+ | undefined
35
+ ): Effect.Effect<RpcClient.Protocol["Type"], FridaSessionError.FridaSessionError, RpcSerialization.RpcSerialization> =>
36
+ RpcClient.Protocol.make(
37
+ Effect.fnUntraced(function* (writeResponse) {
38
+ const serialization = yield* RpcSerialization.RpcSerialization;
39
+ const exportName = options?.exportName ?? "rpc";
40
+ const rpcIsAvailableWhen = options?.rpcIsAvailableWhen;
41
+
42
+ const send = (request: RpcMessage.FromClientEncoded): Effect.Effect<void> => {
43
+ if (request._tag !== "Request") {
44
+ return Effect.void;
45
+ }
46
+
47
+ const parser = serialization.unsafeMake();
48
+ if (!serialization.supportsBigInt) {
49
+ const mutable = request as Types.Mutable<typeof request>;
50
+ mutable.id = request.id.toString();
51
+ }
52
+
53
+ const schema = Schema.Union(Schema.String, Schema.Uint8Array);
54
+ const encode = Function.compose(parser.encode, Schema.encode(schema));
55
+ const decode = Function.compose(Schema.decodeUnknownSync(schema), parser.decode);
56
+
57
+ return encode(request)
58
+ .pipe(Effect.flatMap(script.callExport(exportName)))
59
+ .pipe(Effect.orDie)
60
+ .pipe(
61
+ Effect.flatMap((incoming) => {
62
+ try {
63
+ const responses = decode(incoming) as Array<RpcMessage.FromServerEncoded>;
64
+ if (responses.length === 0) return Effect.void;
65
+ let i = 0;
66
+ return Effect.whileLoop({
67
+ while: () => i < responses.length,
68
+ body: () => writeResponse(responses[i++]),
69
+ step: Function.constVoid,
70
+ });
71
+ } catch (defect) {
72
+ return writeResponse({ _tag: "Defect", defect });
73
+ }
74
+ })
75
+ );
76
+ };
77
+
78
+ if (Predicate.isNotUndefined(rpcIsAvailableWhen)) {
79
+ yield* script.stream
80
+ .pipe(Stream.map(({ message }) => message))
81
+ .pipe(Stream.filter(Predicate.isString))
82
+ .pipe(Stream.takeUntil(rpcIsAvailableWhen))
83
+ .pipe(Stream.runDrain);
84
+ }
85
+
86
+ return {
87
+ send,
88
+ supportsAck: false,
89
+ supportsTransferables: false,
90
+ };
91
+ })
92
+ );
93
+
94
+ /**
95
+ * @since 1.0.0
96
+ * @category Layers
97
+ */
98
+ export const layerProtocolFrida = (
99
+ options?:
100
+ | {
101
+ readonly exportName?: string | undefined;
102
+ readonly rpcIsAvailableWhen?: ((message: string) => boolean) | undefined;
103
+ }
104
+ | undefined
105
+ ): Layer.Layer<
106
+ RpcClient.Protocol,
107
+ FridaSessionError.FridaSessionError,
108
+ RpcSerialization.RpcSerialization | FridaScript.FridaScript
109
+ > =>
110
+ Layer.effect(
111
+ RpcClient.Protocol,
112
+ Effect.flatMap(FridaScript.FridaScript, (script) => makeProtocolFrida(script, options))
113
+ );
@@ -0,0 +1,165 @@
1
+ /**
2
+ * Implements a Frida RPC server protocol for effect using the frida script
3
+ * exports. The reason we don't use the send/recv script channels is because
4
+ * those are shared channels by everybody.
5
+ *
6
+ * @since 1.0.0
7
+ */
8
+
9
+ import type * as Types from "effect/Types";
10
+
11
+ import * as RpcMessage from "@effect/rpc/RpcMessage";
12
+ import * as RpcSerialization from "@effect/rpc/RpcSerialization";
13
+ import * as RpcServer from "@effect/rpc/RpcServer";
14
+ import * as Array from "effect/Array";
15
+ import * as Deferred from "effect/Deferred";
16
+ import * as Effect from "effect/Effect";
17
+ import * as Function from "effect/Function";
18
+ import * as Layer from "effect/Layer";
19
+ import * as Mailbox from "effect/Mailbox";
20
+ import * as Predicate from "effect/Predicate";
21
+ import * as Runtime from "effect/Runtime";
22
+ import * as Schema from "effect/Schema";
23
+
24
+ /**
25
+ * @since 1.0.0
26
+ * @category Protocol
27
+ */
28
+ export const makeProtocolFrida: Effect.Effect<
29
+ {
30
+ protocol: RpcServer.Protocol["Type"];
31
+ rpcExport: (request: string | Uint8Array) => Promise<string | ReadonlyArray<number>>;
32
+ },
33
+ never,
34
+ RpcSerialization.RpcSerialization
35
+ > = Effect.gen(function* () {
36
+ const runtime = yield* Effect.runtime<never>();
37
+ const disconnects = yield* Mailbox.make<number>();
38
+ const serialization = yield* RpcSerialization.RpcSerialization;
39
+
40
+ let clientId = 0;
41
+ const clients = new Map<
42
+ number,
43
+ {
44
+ readonly end: Effect.Effect<void, never, never>;
45
+ readonly write: (bytes: RpcMessage.FromServerEncoded) => Effect.Effect<void>;
46
+ }
47
+ >();
48
+ let writeRequest!: (clientId: number, message: RpcMessage.FromClientEncoded) => Effect.Effect<void>;
49
+
50
+ const rpcExport = async function (request: string | Uint8Array): Promise<string | ReadonlyArray<number>> {
51
+ const id = clientId++;
52
+ const parser = serialization.unsafeMake();
53
+
54
+ const chunks = Array.empty<string | ReadonlyArray<number>>();
55
+ const deferred = await Runtime.runPromise(runtime, Deferred.make<string | ReadonlyArray<number>, never>());
56
+
57
+ const schema = Schema.Union(Schema.String, Schema.Uint8Array);
58
+ const encode = Function.compose(parser.encode, Schema.encodeSync(schema));
59
+ const decode = Function.compose(Schema.decodeUnknownSync(schema), parser.decode);
60
+
61
+ clients.set(id, {
62
+ end: Effect.void,
63
+ write: (response: RpcMessage.FromServerEncoded) => {
64
+ try {
65
+ if (!serialization.supportsBigInt && "requestId" in response) {
66
+ const mutable = response as Types.Mutable<typeof response>;
67
+ mutable.requestId = mutable.requestId.toString();
68
+ }
69
+
70
+ chunks.push(encode(response));
71
+ if (Predicate.isTagged(response, "Chunk")) return Effect.void;
72
+
73
+ const final = Predicate.isString(chunks[0])
74
+ ? chunks.join("")
75
+ : Array.flatten(chunks as Array<ReadonlyArray<number>>);
76
+ return Deferred.succeed(deferred, final);
77
+ } catch (cause) {
78
+ const message = RpcMessage.ResponseDefectEncoded(cause);
79
+ const encoded = encode(message);
80
+ return Deferred.succeed(deferred, encoded);
81
+ }
82
+ },
83
+ });
84
+
85
+ try {
86
+ const decoded = decode(request) as ReadonlyArray<RpcMessage.FromClientEncoded>;
87
+ if (decoded.length === 0) return "";
88
+ let i = 0;
89
+ await Runtime.runPromise(
90
+ runtime,
91
+ Effect.whileLoop({
92
+ while: () => i < decoded.length,
93
+ body: () => writeRequest(id, decoded[i++]),
94
+ step: Function.constVoid,
95
+ })
96
+ );
97
+ } catch (cause) {
98
+ const message = RpcMessage.ResponseDefectEncoded(cause);
99
+ const encoded = encode(message);
100
+ return Promise.resolve(encoded);
101
+ }
102
+
103
+ return Runtime.runPromise(runtime, deferred);
104
+ };
105
+
106
+ const protocol = yield* RpcServer.Protocol.make((writeRequest_) => {
107
+ writeRequest = writeRequest_;
108
+ return Effect.succeed({
109
+ disconnects,
110
+ send: (clientId, response) => {
111
+ const client = clients.get(clientId);
112
+ if (!client) return Effect.void;
113
+ return client.write(response);
114
+ },
115
+ end(clientId) {
116
+ const client = clients.get(clientId);
117
+ if (!client) return Effect.void;
118
+ clients.delete(clientId);
119
+ return client.end;
120
+ },
121
+ initialMessage: Effect.succeedNone,
122
+ supportsAck: false,
123
+ supportsTransferables: false,
124
+ supportsSpanPropagation: false,
125
+ });
126
+ });
127
+
128
+ return { protocol, rpcExport };
129
+ });
130
+
131
+ /**
132
+ * @since 1.0.0
133
+ * @category Protocol
134
+ */
135
+ export const makeProtocolFridaWithExport = (
136
+ options?:
137
+ | {
138
+ readonly exportName?: string | undefined;
139
+ readonly messageOnRpcAvailable?: string | undefined;
140
+ }
141
+ | undefined
142
+ ) =>
143
+ Effect.gen(function* () {
144
+ const { protocol, rpcExport } = yield* makeProtocolFrida;
145
+ rpc.exports[options?.exportName ?? "rpc"] = rpcExport;
146
+ const messageOnRpcAvailable = options?.messageOnRpcAvailable;
147
+ if (Predicate.isNotUndefined(messageOnRpcAvailable)) {
148
+ send(messageOnRpcAvailable);
149
+ }
150
+ return protocol;
151
+ });
152
+
153
+ /**
154
+ * @since 1.0.0
155
+ * @category Layer
156
+ */
157
+ export const layerProtocolFrida = (
158
+ options?:
159
+ | {
160
+ readonly exportName?: string | undefined;
161
+ readonly messageOnRpcAvailable?: string | undefined;
162
+ }
163
+ | undefined
164
+ ): Layer.Layer<RpcServer.Protocol, never, RpcSerialization.RpcSerialization> =>
165
+ Layer.effect(RpcServer.Protocol, makeProtocolFridaWithExport(options));
package/src/index.ts ADDED
@@ -0,0 +1,17 @@
1
+ /**
2
+ * Implements a Frida RPC client protocol for effect using the frida script
3
+ * exports. The reason we don't use the send/recv script channels is because
4
+ * those are shared channels by everybody.
5
+ *
6
+ * @since 1.0.0
7
+ */
8
+ export * as FridaRpcClient from "./FridaRpcClient.js"
9
+
10
+ /**
11
+ * Implements a Frida RPC server protocol for effect using the frida script
12
+ * exports. The reason we don't use the send/recv script channels is because
13
+ * those are shared channels by everybody.
14
+ *
15
+ * @since 1.0.0
16
+ */
17
+ export * as FridaRpcServer from "./FridaRpcServer.js"