appium-ios-remotexpc 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.
Files changed (92) hide show
  1. package/.github/dependabot.yml +38 -0
  2. package/.github/workflows/format-check.yml +43 -0
  3. package/.github/workflows/lint-and-build.yml +40 -0
  4. package/.github/workflows/pr-title.yml +16 -0
  5. package/.github/workflows/publish.js.yml +42 -0
  6. package/.github/workflows/test-validation.yml +40 -0
  7. package/.mocharc.json +8 -0
  8. package/.prettierignore +3 -0
  9. package/.prettierrc +17 -0
  10. package/.releaserc +37 -0
  11. package/CHANGELOG.md +63 -0
  12. package/LICENSE +201 -0
  13. package/README.md +178 -0
  14. package/assets/images/ios-arch.png +0 -0
  15. package/eslint.config.js +45 -0
  16. package/package.json +78 -0
  17. package/scripts/test-tunnel-creation.ts +378 -0
  18. package/src/base-plist-service.ts +83 -0
  19. package/src/base-socket-service.ts +55 -0
  20. package/src/index.ts +34 -0
  21. package/src/lib/apple-tv/constants.ts +83 -0
  22. package/src/lib/apple-tv/errors.ts +31 -0
  23. package/src/lib/apple-tv/tlv/decoder.ts +68 -0
  24. package/src/lib/apple-tv/tlv/encoder.ts +33 -0
  25. package/src/lib/apple-tv/tlv/index.ts +6 -0
  26. package/src/lib/apple-tv/tlv/pairing-tlv.ts +31 -0
  27. package/src/lib/apple-tv/types.ts +58 -0
  28. package/src/lib/apple-tv/utils/buffer-utils.ts +90 -0
  29. package/src/lib/apple-tv/utils/index.ts +2 -0
  30. package/src/lib/apple-tv/utils/uuid-generator.ts +43 -0
  31. package/src/lib/lockdown/index.ts +468 -0
  32. package/src/lib/pair-record/index.ts +8 -0
  33. package/src/lib/pair-record/pair-record.ts +133 -0
  34. package/src/lib/plist/binary-plist-creator.ts +571 -0
  35. package/src/lib/plist/binary-plist-parser.ts +587 -0
  36. package/src/lib/plist/constants.ts +53 -0
  37. package/src/lib/plist/index.ts +54 -0
  38. package/src/lib/plist/length-based-splitter.ts +326 -0
  39. package/src/lib/plist/plist-creator.ts +42 -0
  40. package/src/lib/plist/plist-decoder.ts +135 -0
  41. package/src/lib/plist/plist-encoder.ts +36 -0
  42. package/src/lib/plist/plist-parser.ts +144 -0
  43. package/src/lib/plist/plist-service.ts +231 -0
  44. package/src/lib/plist/unified-plist-creator.ts +19 -0
  45. package/src/lib/plist/unified-plist-parser.ts +25 -0
  46. package/src/lib/plist/utils.ts +376 -0
  47. package/src/lib/remote-xpc/constants.ts +22 -0
  48. package/src/lib/remote-xpc/handshake-frames.ts +377 -0
  49. package/src/lib/remote-xpc/handshake.ts +152 -0
  50. package/src/lib/remote-xpc/remote-xpc-connection.ts +461 -0
  51. package/src/lib/remote-xpc/xpc-protocol.ts +412 -0
  52. package/src/lib/tunnel/index.ts +253 -0
  53. package/src/lib/tunnel/packet-stream-client.ts +185 -0
  54. package/src/lib/tunnel/packet-stream-server.ts +133 -0
  55. package/src/lib/tunnel/tunnel-api-client.ts +234 -0
  56. package/src/lib/tunnel/tunnel-registry-server.ts +410 -0
  57. package/src/lib/types.ts +291 -0
  58. package/src/lib/usbmux/index.ts +630 -0
  59. package/src/lib/usbmux/usbmux-decoder.ts +66 -0
  60. package/src/lib/usbmux/usbmux-encoder.ts +55 -0
  61. package/src/service-connection.ts +79 -0
  62. package/src/services/index.ts +15 -0
  63. package/src/services/ios/base-service.ts +81 -0
  64. package/src/services/ios/diagnostic-service/index.ts +241 -0
  65. package/src/services/ios/diagnostic-service/keys.ts +770 -0
  66. package/src/services/ios/syslog-service/index.ts +387 -0
  67. package/src/services/ios/tunnel-service/index.ts +88 -0
  68. package/src/services.ts +81 -0
  69. package/test/integration/diagnostics-test.ts +44 -0
  70. package/test/integration/read-pair-record-test.ts +39 -0
  71. package/test/integration/tunnel-test.ts +104 -0
  72. package/test/unit/apple-tv/tlv/decoder.spec.ts +144 -0
  73. package/test/unit/apple-tv/tlv/encoder.spec.ts +91 -0
  74. package/test/unit/apple-tv/tlv/pairing-tlv.spec.ts +101 -0
  75. package/test/unit/apple-tv/tlv/tlv-integration.spec.ts +146 -0
  76. package/test/unit/apple-tv/utils/buffer-utils.spec.ts +74 -0
  77. package/test/unit/apple-tv/utils/uuid-generator.spec.ts +39 -0
  78. package/test/unit/fixtures/index.ts +88 -0
  79. package/test/unit/fixtures/usbmuxconnectmessage.bin +0 -0
  80. package/test/unit/fixtures/usbmuxlistdevicemessage.bin +0 -0
  81. package/test/unit/plist/error-handling.spec.ts +101 -0
  82. package/test/unit/plist/fixtures/sample.binary.plist +0 -0
  83. package/test/unit/plist/fixtures/sample.xml.plist +38 -0
  84. package/test/unit/plist/plist-parser.spec.ts +283 -0
  85. package/test/unit/plist/plist.spec.ts +205 -0
  86. package/test/unit/plist/tag-position-handling.spec.ts +90 -0
  87. package/test/unit/plist/unified-plist-parser.spec.ts +227 -0
  88. package/test/unit/plist/utils.spec.ts +249 -0
  89. package/test/unit/plist/xml-cleaning.spec.ts +60 -0
  90. package/test/unit/tunnel/tunnel-registry-server.spec.ts +194 -0
  91. package/test/unit/usbmux/usbmux-specs.ts +71 -0
  92. package/tsconfig.json +36 -0
@@ -0,0 +1,185 @@
1
+ import { logger } from '@appium/support';
2
+ import { EventEmitter } from 'events';
3
+ import { type Socket, createConnection } from 'net';
4
+ import type { PacketConsumer, PacketData } from 'tuntap-bridge';
5
+
6
+ const log = logger.getLogger('PacketStreamClient');
7
+
8
+ /**
9
+ * Constants for packet stream protocol
10
+ */
11
+ const PACKET_LENGTH_PREFIX_SIZE = 10;
12
+
13
+ /**
14
+ * Client that connects to a PacketStreamServer to receive packet data
15
+ * Implements the PacketSource interface required by SyslogService
16
+ */
17
+ export class PacketStreamClient extends EventEmitter {
18
+ private socket: Socket | null = null;
19
+ private readonly packetConsumers: Set<PacketConsumer> = new Set();
20
+ private buffer: Buffer = Buffer.alloc(0);
21
+ private connected = false;
22
+
23
+ constructor(
24
+ private readonly host: string,
25
+ private readonly port: number,
26
+ ) {
27
+ super();
28
+ }
29
+
30
+ async connect(): Promise<void> {
31
+ if (this.connected) {
32
+ log.info('Already connected');
33
+ return;
34
+ }
35
+
36
+ return new Promise((resolve, reject) => {
37
+ this.socket = createConnection(
38
+ { host: this.host, port: this.port },
39
+ () => {
40
+ log.info(
41
+ `Connected to packet stream server at ${this.host}:${this.port}`,
42
+ );
43
+ this.connected = true;
44
+ resolve();
45
+ },
46
+ );
47
+
48
+ this.socket.on('data', (data) => {
49
+ this.handleData(data);
50
+ });
51
+
52
+ this.socket.once('close', () => {
53
+ log.info('Disconnected from packet stream server');
54
+ this.connected = false;
55
+ this.emit('close');
56
+ });
57
+
58
+ this.socket.on('error', (err) => {
59
+ log.error(`Socket error: ${err}`);
60
+ this.connected = false;
61
+ if (!this.socket) {
62
+ reject(err);
63
+ } else {
64
+ this.emit('error', err);
65
+ }
66
+ });
67
+ });
68
+ }
69
+
70
+ isConnected(): boolean {
71
+ return this.connected;
72
+ }
73
+
74
+ async disconnect(): Promise<void> {
75
+ if (this.socket && !this.socket.destroyed) {
76
+ this.socket.destroy();
77
+ this.socket = null;
78
+ }
79
+ this.connected = false;
80
+ this.packetConsumers.clear();
81
+ }
82
+
83
+ addPacketConsumer(consumer: PacketConsumer): void {
84
+ this.packetConsumers.add(consumer);
85
+ }
86
+
87
+ removePacketConsumer(consumer: PacketConsumer): void {
88
+ this.packetConsumers.delete(consumer);
89
+ }
90
+
91
+ /**
92
+ * Handle incoming data from the socket
93
+ */
94
+ private handleData(data: Buffer): void {
95
+ this.buffer = Buffer.concat([this.buffer, data]);
96
+ this.processBuffer();
97
+ }
98
+
99
+ /**
100
+ * Process buffered data to extract complete messages
101
+ */
102
+ private processBuffer(): void {
103
+ while (this.buffer.length >= PACKET_LENGTH_PREFIX_SIZE) {
104
+ const messageLength = this.extractMessageLength();
105
+
106
+ if (messageLength === null) {
107
+ // Invalid length, reset buffer
108
+ this.buffer = Buffer.alloc(0);
109
+ break;
110
+ }
111
+
112
+ const totalMessageSize = PACKET_LENGTH_PREFIX_SIZE + messageLength;
113
+
114
+ if (this.buffer.length < totalMessageSize) {
115
+ // Wait for more data
116
+ break;
117
+ }
118
+
119
+ const messageData = this.buffer.slice(
120
+ PACKET_LENGTH_PREFIX_SIZE,
121
+ totalMessageSize,
122
+ );
123
+ this.buffer = this.buffer.slice(totalMessageSize);
124
+
125
+ this.processMessage(messageData);
126
+ }
127
+ }
128
+
129
+ /**
130
+ * Extract message length from buffer
131
+ * @returns Message length or null if invalid
132
+ */
133
+ private extractMessageLength(): number | null {
134
+ const lengthStr = this.buffer
135
+ .slice(0, PACKET_LENGTH_PREFIX_SIZE)
136
+ .toString();
137
+ const messageLength = parseInt(lengthStr, 10);
138
+
139
+ if (isNaN(messageLength)) {
140
+ log.error('Invalid message length, clearing buffer');
141
+ return null;
142
+ }
143
+
144
+ return messageLength;
145
+ }
146
+
147
+ /**
148
+ * Process a single message
149
+ */
150
+ private processMessage(messageData: Buffer): void {
151
+ try {
152
+ const packet = this.parsePacket(messageData);
153
+ this.notifyConsumers(packet);
154
+ } catch (err) {
155
+ log.error(`Error processing message: ${err}`);
156
+ }
157
+ }
158
+
159
+ /**
160
+ * Parse packet data from message buffer
161
+ */
162
+ private parsePacket(messageData: Buffer): PacketData {
163
+ const packet: PacketData = JSON.parse(messageData.toString());
164
+
165
+ // Reconstruct Buffer from JSON
166
+ if (packet.payload && typeof packet.payload === 'object') {
167
+ packet.payload = Buffer.from(packet.payload);
168
+ }
169
+
170
+ return packet;
171
+ }
172
+
173
+ /**
174
+ * Notify all packet consumers
175
+ */
176
+ private notifyConsumers(packet: PacketData): void {
177
+ for (const consumer of this.packetConsumers) {
178
+ try {
179
+ consumer.onPacket(packet);
180
+ } catch (err) {
181
+ log.error(`Error in packet consumer: ${err}`);
182
+ }
183
+ }
184
+ }
185
+ }
@@ -0,0 +1,133 @@
1
+ import { logger } from '@appium/support';
2
+ import { EventEmitter } from 'events';
3
+ import { type Server, type Socket, createServer } from 'net';
4
+ import type { PacketConsumer, PacketData } from 'tuntap-bridge';
5
+
6
+ const log = logger.getLogger('PacketStreamServer');
7
+
8
+ /**
9
+ * Interface for serialized packet message
10
+ */
11
+ interface SerializedPacketMessage {
12
+ length: string;
13
+ data: string;
14
+ }
15
+
16
+ /**
17
+ * Server that exposes packet streaming from a tunnel over TCP
18
+ * This allows cross-process access to tunnel packet streams
19
+ */
20
+ export class PacketStreamServer extends EventEmitter {
21
+ private server: Server | null = null;
22
+ private readonly clients: Set<Socket> = new Set();
23
+ private packetConsumer: PacketConsumer | null = null;
24
+
25
+ constructor(private readonly port: number) {
26
+ super();
27
+ }
28
+
29
+ /**
30
+ * Start the packet stream server
31
+ * @throws {Error} If server is already started
32
+ */
33
+ async start(): Promise<void> {
34
+ if (this.server) {
35
+ throw new Error('Server already started');
36
+ }
37
+
38
+ this.server = createServer((client) => {
39
+ this.handleClientConnection(client);
40
+ });
41
+
42
+ this.packetConsumer = this.createPacketConsumer();
43
+
44
+ return new Promise((resolve, reject) => {
45
+ this.server!.listen(this.port, () => {
46
+ log.info(`Packet stream server listening on port ${this.port}`);
47
+ resolve();
48
+ });
49
+
50
+ this.server!.on('error', reject);
51
+ });
52
+ }
53
+
54
+ async stop(): Promise<void> {
55
+ for (const client of this.clients) {
56
+ client.destroy();
57
+ }
58
+ this.clients.clear();
59
+
60
+ if (this.server) {
61
+ return new Promise((resolve) => {
62
+ this.server?.close(() => {
63
+ this.server = null;
64
+ resolve();
65
+ });
66
+ });
67
+ }
68
+ }
69
+
70
+ getPacketConsumer(): PacketConsumer | null {
71
+ return this.packetConsumer;
72
+ }
73
+
74
+ /**
75
+ * Handle new client connection
76
+ */
77
+ private handleClientConnection(client: Socket): void {
78
+ log.info(`Client connected from ${client.remoteAddress}`);
79
+ this.clients.add(client);
80
+
81
+ client.on('close', () => {
82
+ log.info(`Client disconnected from ${client.remoteAddress}`);
83
+ this.clients.delete(client);
84
+ });
85
+
86
+ client.on('error', (err) => {
87
+ log.error(`Client error: ${err}`);
88
+ this.clients.delete(client);
89
+ });
90
+ }
91
+
92
+ /**
93
+ * Create packet consumer that broadcasts packets to all connected clients
94
+ */
95
+ private createPacketConsumer(): PacketConsumer {
96
+ return {
97
+ onPacket: (packet: PacketData) => {
98
+ this.broadcastPacket(packet);
99
+ },
100
+ };
101
+ }
102
+
103
+ /**
104
+ * Broadcast packet to all connected clients
105
+ */
106
+ private broadcastPacket(packet: PacketData): void {
107
+ try {
108
+ const serialized = JSON.stringify(packet);
109
+ const message = this.createMessage(serialized);
110
+
111
+ for (const client of this.clients) {
112
+ if (!client.destroyed) {
113
+ client.write(message, (err) => {
114
+ if (err) {
115
+ log.error(`Failed to write to client: ${err}`);
116
+ this.clients.delete(client);
117
+ }
118
+ });
119
+ }
120
+ }
121
+ } catch (err) {
122
+ log.error(`Failed to broadcast packet: ${err}`);
123
+ }
124
+ }
125
+
126
+ /**
127
+ * Create a message buffer with length prefix
128
+ */
129
+ private createMessage(data: string): Buffer {
130
+ const lengthPrefix = data.length.toString().padStart(10, '0');
131
+ return Buffer.concat([Buffer.from(lengthPrefix), Buffer.from(data)]);
132
+ }
133
+ }
@@ -0,0 +1,234 @@
1
+ import { logger } from '@appium/support';
2
+
3
+ import type { TunnelRegistry, TunnelRegistryEntry } from '../types.js';
4
+
5
+ const log = logger.getLogger('TunnelApiClient');
6
+
7
+ /**
8
+ * API client for tunnel registry operations
9
+ * This client handles communication with the API server for tunnel data
10
+ */
11
+ export class TunnelApiClient {
12
+ private apiBaseUrl: string;
13
+
14
+ /**
15
+ * Create a new TunnelApiClient
16
+ * @param apiBaseUrl - Base URL for the API server
17
+ */
18
+ constructor(apiBaseUrl: string = 'http://localhost:42314/remotexpc/tunnels') {
19
+ this.apiBaseUrl = apiBaseUrl;
20
+ }
21
+
22
+ /**
23
+ * Set the API base URL
24
+ * @param url - New base URL for the API server
25
+ */
26
+ setApiBaseUrl(url: string): void {
27
+ this.apiBaseUrl = url;
28
+ }
29
+
30
+ /**
31
+ * Get the API base URL
32
+ * @returns The current API base URL
33
+ */
34
+ getApiBaseUrl(): string {
35
+ return this.apiBaseUrl;
36
+ }
37
+
38
+ /**
39
+ * Fetch all tunnel registry data from the API server
40
+ * @returns The complete tunnel registry
41
+ */
42
+ async fetchRegistry(): Promise<TunnelRegistry> {
43
+ try {
44
+ const response = await fetch(this.apiBaseUrl);
45
+
46
+ if (!response.ok) {
47
+ throw new Error(`API request failed with status: ${response.status}`);
48
+ }
49
+
50
+ return (await response.json()) as TunnelRegistry;
51
+ } catch (error) {
52
+ log.warn(`Failed to fetch tunnel registry from API: ${error}`);
53
+ // Return empty registry as fallback
54
+ return {
55
+ tunnels: {},
56
+ metadata: {
57
+ lastUpdated: new Date().toISOString(),
58
+ totalTunnels: 0,
59
+ activeTunnels: 0,
60
+ },
61
+ };
62
+ }
63
+ }
64
+
65
+ /**
66
+ * Get a specific tunnel by UDID
67
+ * @param udid - Device UDID
68
+ * @returns Tunnel registry entry or null if not found
69
+ */
70
+ async getTunnelByUdid(udid: string): Promise<TunnelRegistryEntry | null> {
71
+ try {
72
+ const response = await fetch(`${this.apiBaseUrl}/${udid}`);
73
+
74
+ if (response.status === 404) {
75
+ return null;
76
+ }
77
+
78
+ if (!response.ok) {
79
+ throw new Error(`API request failed with status: ${response.status}`);
80
+ }
81
+
82
+ return (await response.json()) as TunnelRegistryEntry;
83
+ } catch (error) {
84
+ log.warn(`Failed to fetch tunnel for UDID ${udid}: ${error}`);
85
+ return null;
86
+ }
87
+ }
88
+
89
+ /**
90
+ * Get tunnel by device ID
91
+ * @param deviceId - Device ID
92
+ * @returns Tunnel registry entry or null if not found
93
+ */
94
+ async getTunnelByDeviceId(
95
+ deviceId: number,
96
+ ): Promise<TunnelRegistryEntry | null> {
97
+ try {
98
+ const response = await fetch(`${this.apiBaseUrl}/device/${deviceId}`);
99
+
100
+ if (response.status === 404) {
101
+ return null;
102
+ }
103
+
104
+ if (!response.ok) {
105
+ throw new Error(`API request failed with status: ${response.status}`);
106
+ }
107
+
108
+ return (await response.json()) as TunnelRegistryEntry;
109
+ } catch (error) {
110
+ log.warn(`Failed to fetch tunnel for device ID ${deviceId}: ${error}`);
111
+ return null;
112
+ }
113
+ }
114
+
115
+ /**
116
+ * Get all tunnels
117
+ * @returns Array of tunnel registry entries
118
+ */
119
+ async getAllTunnels(): Promise<TunnelRegistryEntry[]> {
120
+ try {
121
+ const registry = await this.fetchRegistry();
122
+ return Object.values(registry.tunnels);
123
+ } catch (error) {
124
+ log.warn(`Failed to fetch all tunnels: ${error}`);
125
+ return [];
126
+ }
127
+ }
128
+
129
+ /**
130
+ * Check if a tunnel exists for a specific UDID
131
+ * @param udid - Device UDID
132
+ * @returns True if tunnel exists, false otherwise
133
+ */
134
+ async hasTunnel(udid: string): Promise<boolean> {
135
+ const tunnel = await this.getTunnelByUdid(udid);
136
+ return tunnel !== null;
137
+ }
138
+
139
+ /**
140
+ * Get registry metadata
141
+ * @returns Registry metadata
142
+ */
143
+ async getMetadata(): Promise<TunnelRegistry['metadata']> {
144
+ try {
145
+ const registry = await this.fetchRegistry();
146
+ return registry.metadata;
147
+ } catch (error) {
148
+ log.warn(`Failed to fetch registry metadata: ${error}`);
149
+ return {
150
+ lastUpdated: new Date().toISOString(),
151
+ totalTunnels: 0,
152
+ activeTunnels: 0,
153
+ };
154
+ }
155
+ }
156
+
157
+ /**
158
+ * Get tunnel connection details formatted for easy use
159
+ * @param udid - Device UDID
160
+ * @returns Connection details or null if tunnel not found
161
+ */
162
+ async getTunnelConnection(udid: string): Promise<{
163
+ host: string;
164
+ port: number;
165
+ udid: string;
166
+ packetStreamPort: number;
167
+ } | null> {
168
+ const tunnel = await this.getTunnelByUdid(udid);
169
+ if (!tunnel) {
170
+ return null;
171
+ }
172
+
173
+ return {
174
+ host: tunnel.address,
175
+ port: tunnel.rsdPort,
176
+ udid: tunnel.udid,
177
+ packetStreamPort: tunnel.packetStreamPort,
178
+ };
179
+ }
180
+
181
+ /**
182
+ * List all available device UDIDs with tunnels
183
+ * @returns Array of device UDIDs
184
+ */
185
+ async getAvailableDevices(): Promise<string[]> {
186
+ try {
187
+ const registry = await this.fetchRegistry();
188
+ return Object.keys(registry.tunnels);
189
+ } catch (error) {
190
+ log.warn(`Failed to fetch available devices: ${error}`);
191
+ return [];
192
+ }
193
+ }
194
+
195
+ /**
196
+ * Update or create a tunnel entry
197
+ * @param entry - Tunnel registry entry to update or create
198
+ * @returns True if successful, false otherwise
199
+ */
200
+ async updateTunnel(entry: TunnelRegistryEntry): Promise<boolean> {
201
+ try {
202
+ const response = await fetch(`${this.apiBaseUrl}/${entry.udid}`, {
203
+ method: 'PUT',
204
+ headers: {
205
+ 'Content-Type': 'application/json',
206
+ },
207
+ body: JSON.stringify(entry),
208
+ });
209
+
210
+ return response.ok;
211
+ } catch (error) {
212
+ log.error(`Failed to update tunnel for UDID ${entry.udid}: ${error}`);
213
+ return false;
214
+ }
215
+ }
216
+
217
+ /**
218
+ * Delete a tunnel entry
219
+ * @param udid - Device UDID
220
+ * @returns True if successful, false otherwise
221
+ */
222
+ async deleteTunnel(udid: string): Promise<boolean> {
223
+ try {
224
+ const response = await fetch(`${this.apiBaseUrl}/${udid}`, {
225
+ method: 'DELETE',
226
+ });
227
+
228
+ return response.ok;
229
+ } catch (error) {
230
+ log.error(`Failed to delete tunnel for UDID ${udid}: ${error}`);
231
+ return false;
232
+ }
233
+ }
234
+ }