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,461 @@
1
+ import { logger } from '@appium/support';
2
+ import net from 'node:net';
3
+
4
+ import Handshake from './handshake.js';
5
+
6
+ const log = logger.getLogger('RemoteXpcConnection');
7
+
8
+ // Timeout constants
9
+ const CONNECTION_TIMEOUT_MS = 30000; // 30 seconds
10
+ const SERVICE_EXTRACTION_TIMEOUT_MS = 5000; // 5 seconds
11
+ const HANDSHAKE_DELAY_MS = 100; // 100 milliseconds
12
+ const SERVICE_AFTER_HANDSHAKE_TIMEOUT_MS = 10000; // 10 seconds
13
+ const SOCKET_CLOSE_TIMEOUT_MS = 1000; // 1 second
14
+ const SOCKET_END_TIMEOUT_MS = 500; // 0.5 seconds
15
+ const SOCKET_WRITE_TIMEOUT_MS = 500; // 0.5 seconds
16
+
17
+ interface Service {
18
+ serviceName: string;
19
+ port: string;
20
+ }
21
+
22
+ interface ServicesResponse {
23
+ services: Service[];
24
+ }
25
+
26
+ type ConnectionTimeout = NodeJS.Timeout;
27
+ type ServiceExtractionTimeout = NodeJS.Timeout;
28
+
29
+ class RemoteXpcConnection {
30
+ private readonly _address: [string, number];
31
+ private _socket: net.Socket | undefined;
32
+ private _handshake: Handshake | undefined;
33
+ private _isConnected: boolean;
34
+ private _services: Service[] | undefined;
35
+
36
+ constructor(address: [string, number]) {
37
+ this._address = address;
38
+ this._socket = undefined;
39
+ this._handshake = undefined;
40
+ this._isConnected = false;
41
+ this._services = undefined;
42
+ }
43
+
44
+ /**
45
+ * Connect to the remote device and perform handshake
46
+ * @returns Promise that resolves with the list of available services
47
+ */
48
+ async connect(): Promise<ServicesResponse> {
49
+ if (this._isConnected) {
50
+ throw new Error('Already connected');
51
+ }
52
+
53
+ return new Promise<ServicesResponse>((resolve, reject) => {
54
+ // Set a timeout for the entire connection process
55
+ const connectionTimeout: ConnectionTimeout = setTimeout(() => {
56
+ if (this._socket) {
57
+ this._socket.destroy();
58
+ }
59
+ reject(
60
+ new Error(
61
+ `Connection timed out after ${CONNECTION_TIMEOUT_MS / 1000} seconds`,
62
+ ),
63
+ );
64
+ }, CONNECTION_TIMEOUT_MS);
65
+
66
+ // Set a timeout for service extraction
67
+ let serviceExtractionTimeout: ServiceExtractionTimeout;
68
+
69
+ const clearTimeouts = (): void => {
70
+ clearTimeout(connectionTimeout);
71
+ if (serviceExtractionTimeout) {
72
+ clearTimeout(serviceExtractionTimeout);
73
+ }
74
+ };
75
+
76
+ try {
77
+ this._socket = net.connect({
78
+ host: this._address[0],
79
+ port: this._address[1],
80
+ family: 6,
81
+ });
82
+
83
+ this._socket.setNoDelay(true);
84
+ this._socket.setKeepAlive(true);
85
+
86
+ // Buffer to accumulate data
87
+ let accumulatedData = Buffer.alloc(0);
88
+
89
+ this._socket.once('error', (error: Error) => {
90
+ log.error(`Connection error: ${error}`);
91
+ this._isConnected = false;
92
+ clearTimeouts();
93
+ reject(error);
94
+ });
95
+
96
+ // Handle incoming data
97
+ this._socket.on('data', (data: Buffer | string) => {
98
+ if (Buffer.isBuffer(data) || typeof data === 'string') {
99
+ const buffer = Buffer.isBuffer(data)
100
+ ? data
101
+ : Buffer.from(data, 'hex');
102
+
103
+ // Accumulate data
104
+ accumulatedData = Buffer.concat([accumulatedData, buffer]);
105
+
106
+ // Check if we have enough data to extract services
107
+ // Don't rely solely on buffer length, also check for service patterns
108
+ const dataStr = accumulatedData.toString('utf8');
109
+ if (dataStr.includes('com.apple') && dataStr.includes('Port')) {
110
+ try {
111
+ const servicesResponse = extractServices(dataStr);
112
+
113
+ // Only resolve if we found at least one service
114
+ if (servicesResponse.services.length > 0) {
115
+ this._services = servicesResponse.services;
116
+ log.info(
117
+ `Extracted ${servicesResponse.services.length} services`,
118
+ );
119
+ clearTimeouts();
120
+ resolve(servicesResponse);
121
+ } else if (!serviceExtractionTimeout) {
122
+ // Set a timeout to resolve with whatever we have if no more data comes
123
+ serviceExtractionTimeout = setTimeout(() => {
124
+ log.warn(
125
+ 'Service extraction timeout reached, resolving with current data',
126
+ );
127
+ const finalResponse = extractServices(
128
+ accumulatedData.toString('utf8'),
129
+ );
130
+ this._services = finalResponse.services;
131
+ clearTimeouts();
132
+ resolve(finalResponse);
133
+ }, SERVICE_EXTRACTION_TIMEOUT_MS);
134
+ }
135
+ } catch (error) {
136
+ log.warn(
137
+ `Error extracting services: ${error}, continuing to collect data`,
138
+ );
139
+ }
140
+ }
141
+ }
142
+ });
143
+
144
+ this._socket.on('close', () => {
145
+ log.info('Socket closed');
146
+ this._isConnected = false;
147
+ clearTimeouts();
148
+
149
+ // If we haven't resolved yet, reject with an error
150
+ if (this._services === undefined) {
151
+ reject(
152
+ new Error('Connection closed before services were extracted'),
153
+ );
154
+ }
155
+ });
156
+
157
+ this._socket.once('connect', async () => {
158
+ try {
159
+ this._isConnected = true;
160
+ if (this._socket) {
161
+ this._handshake = new Handshake(this._socket);
162
+
163
+ // Add a small delay before performing handshake to ensure socket is ready
164
+ await new Promise<void>((resolve) =>
165
+ setTimeout(resolve, HANDSHAKE_DELAY_MS),
166
+ );
167
+
168
+ // Once handshake is successful we can get
169
+ // peer-info and get ports for lockdown in RSD
170
+ await this._handshake.perform();
171
+
172
+ // Set a timeout for service extraction
173
+ setTimeout(async () => {
174
+ if (this._services === undefined) {
175
+ log.warn(
176
+ 'No services received after handshake, closing connection',
177
+ );
178
+ try {
179
+ await this.close();
180
+ } catch (err) {
181
+ log.error(`Error closing connection: ${err}`);
182
+ }
183
+ reject(new Error('No services received after handshake'));
184
+ }
185
+ }, SERVICE_AFTER_HANDSHAKE_TIMEOUT_MS);
186
+ }
187
+ } catch (error) {
188
+ log.error(`Handshake failed: ${error}`);
189
+ clearTimeouts();
190
+ await this.close();
191
+ reject(error);
192
+ }
193
+ });
194
+ } catch (error) {
195
+ log.error(`Failed to create connection: ${error}`);
196
+ clearTimeouts();
197
+ reject(error);
198
+ }
199
+ });
200
+ }
201
+
202
+ /**
203
+ * Close the connection
204
+ */
205
+ async close(): Promise<void> {
206
+ if (!this._socket) {
207
+ return Promise.resolve();
208
+ }
209
+
210
+ // Immediately mark as disconnected to prevent further operations
211
+ this._isConnected = false;
212
+
213
+ return new Promise<void>((resolve) => {
214
+ // Set a shorter timeout for socket closing
215
+ const closeTimeout = setTimeout(() => {
216
+ log.warn('Socket close timed out, destroying socket');
217
+ this.forceCleanup();
218
+ resolve();
219
+ }, SOCKET_CLOSE_TIMEOUT_MS);
220
+
221
+ // Listen for the close event
222
+ if (this._socket) {
223
+ this._socket.once('close', () => {
224
+ log.debug('Socket closed successfully');
225
+ clearTimeout(closeTimeout);
226
+ this.cleanupResources();
227
+ resolve();
228
+ });
229
+
230
+ // Add an error handler specifically for the close operation
231
+ this._socket.once('error', (err) => {
232
+ log.error(`Socket error during close: ${err.message}`);
233
+ // Don't wait for timeout, force cleanup immediately
234
+ clearTimeout(closeTimeout);
235
+ this.forceCleanup();
236
+ resolve();
237
+ });
238
+ }
239
+
240
+ try {
241
+ // First remove all data listeners to prevent parsing during close
242
+ this.cleanupSocket();
243
+
244
+ if (this._socket) {
245
+ // Set a small write timeout to prevent hanging
246
+ this._socket.setTimeout(SOCKET_WRITE_TIMEOUT_MS);
247
+
248
+ // End the socket with a small empty buffer to flush any pending data
249
+ this._socket.end(Buffer.alloc(0), () => {
250
+ // If end completes successfully, the 'close' event will handle cleanup
251
+ // But set a short timeout just in case 'close' doesn't fire
252
+ setTimeout(() => {
253
+ if (this._socket) {
254
+ log.debug(
255
+ 'Socket end completed but close event not fired, forcing cleanup',
256
+ );
257
+ clearTimeout(closeTimeout);
258
+ this.forceCleanup();
259
+ resolve();
260
+ }
261
+ }, SOCKET_END_TIMEOUT_MS);
262
+ });
263
+ } else {
264
+ clearTimeout(closeTimeout);
265
+ this.cleanupResources();
266
+ resolve();
267
+ }
268
+ } catch (error) {
269
+ log.error(
270
+ `Unexpected error during close: ${error instanceof Error ? error.message : String(error)}`,
271
+ );
272
+ clearTimeout(closeTimeout);
273
+ this.forceCleanup();
274
+ resolve();
275
+ }
276
+ });
277
+ }
278
+
279
+ /**
280
+ * Get the list of available services
281
+ * @returns Array of available services
282
+ */
283
+ getServices(): Service[] {
284
+ if (!this._services) {
285
+ throw new Error('Not connected or services not available');
286
+ }
287
+ return this._services;
288
+ }
289
+
290
+ /**
291
+ * List all available services
292
+ * @returns Array of all available services
293
+ */
294
+ listAllServices(): Service[] {
295
+ return this.getServices();
296
+ }
297
+
298
+ /**
299
+ * Find a service by name
300
+ * @param serviceName The name of the service to find
301
+ * @returns The service or throws an error if not found
302
+ */
303
+ findService(serviceName: string): Service {
304
+ const services = this.getServices();
305
+ const service = services.find(
306
+ (service) => service.serviceName === serviceName,
307
+ );
308
+ if (!service) {
309
+ throw new Error(`Service ${serviceName} not found,
310
+ Check if the device is locked.`);
311
+ }
312
+ return service;
313
+ }
314
+
315
+ /**
316
+ * Remove all listeners from the socket to prevent memory leaks
317
+ */
318
+ private cleanupSocket(): void {
319
+ if (this._socket) {
320
+ try {
321
+ // Store references to the listeners we want to keep
322
+ const closeListeners = this._socket.listeners('close') as Array<
323
+ (...args: any[]) => void
324
+ >;
325
+ const errorListeners = this._socket.listeners('error') as Array<
326
+ (...args: any[]) => void
327
+ >;
328
+
329
+ // Remove all listeners
330
+ this._socket.removeAllListeners();
331
+
332
+ // Re-add only the close and error listeners we need for cleanup
333
+ for (const listener of closeListeners) {
334
+ this._socket.once('close', listener);
335
+ }
336
+
337
+ for (const listener of errorListeners) {
338
+ this._socket.once('error', listener);
339
+ }
340
+
341
+ log.debug('Successfully removed socket data listeners');
342
+ } catch (error) {
343
+ log.error(
344
+ `Error removing socket listeners: ${error instanceof Error ? error.message : String(error)}`,
345
+ );
346
+ }
347
+ }
348
+ }
349
+
350
+ /**
351
+ * Clean up all resources
352
+ */
353
+ private cleanupResources(): void {
354
+ this._socket = undefined;
355
+ this._isConnected = false;
356
+ this._handshake = undefined;
357
+ this._services = undefined;
358
+ }
359
+
360
+ /**
361
+ * Force cleanup by destroying the socket and cleaning up resources
362
+ */
363
+ private forceCleanup(): void {
364
+ try {
365
+ if (this._socket) {
366
+ // Destroy the socket forcefully
367
+ this._socket.destroy();
368
+ log.debug('Socket forcefully destroyed');
369
+ }
370
+ } catch (error) {
371
+ log.error(
372
+ `Error destroying socket: ${error instanceof Error ? error.message : String(error)}`,
373
+ );
374
+ } finally {
375
+ this.cleanupResources();
376
+ }
377
+ }
378
+ }
379
+
380
+ /**
381
+ * Extract services from the response
382
+ * @param response The response string to parse
383
+ * @returns Object containing the extracted services
384
+ */
385
+ function extractServices(response: string): ServicesResponse {
386
+ // More robust regex that handles various formats of service names and port specifications
387
+ const serviceRegex = /com\.apple(?:\.[\w-]+)+/g;
388
+ const portRegex = /Port[^0-9]*(\d+)/g;
389
+
390
+ interface Match {
391
+ value: string;
392
+ index: number;
393
+ }
394
+
395
+ // First, collect all service names
396
+ const serviceMatches: Match[] = [];
397
+ let match: RegExpExecArray | null;
398
+ while ((match = serviceRegex.exec(response)) !== null) {
399
+ serviceMatches.push({ value: match[0], index: match.index });
400
+ }
401
+
402
+ // Then, collect all port numbers
403
+ const portMatches: Match[] = [];
404
+ while ((match = portRegex.exec(response)) !== null) {
405
+ if (match[1]) {
406
+ // Ensure we have a captured port number
407
+ portMatches.push({ value: match[1], index: match.index });
408
+ }
409
+ }
410
+
411
+ // Sort both arrays by index to maintain order
412
+ serviceMatches.sort((a, b) => a.index - b.index);
413
+ portMatches.sort((a, b) => a.index - b.index);
414
+
415
+ // Log the extracted data for debugging
416
+ log.debug(
417
+ `Found ${serviceMatches.length} services and ${portMatches.length} ports`,
418
+ );
419
+
420
+ // Create a mapping of services to ports
421
+ const services: Service[] = [];
422
+
423
+ // Assign a port to each service based on proximity in the response
424
+ for (let i = 0; i < serviceMatches.length; i++) {
425
+ const serviceName = serviceMatches[i].value;
426
+ const serviceIndex = serviceMatches[i].index;
427
+
428
+ // Find the closest port after this service
429
+ let closestPort = '';
430
+ let closestDistance = Number.MAX_SAFE_INTEGER;
431
+
432
+ for (const portMatch of portMatches) {
433
+ // Only consider ports that come after the service in the response
434
+ if (portMatch.index > serviceIndex) {
435
+ const distance = portMatch.index - serviceIndex;
436
+
437
+ // If this port is closer than the current closest, update
438
+ if (distance < closestDistance) {
439
+ closestDistance = distance;
440
+ closestPort = portMatch.value;
441
+
442
+ // If the port is very close (within 200 chars), we can be confident it's the right one
443
+ if (distance < 200) {
444
+ break;
445
+ }
446
+ }
447
+ }
448
+ }
449
+
450
+ // Add the service with its port (or empty string if no port found)
451
+ services.push({
452
+ serviceName,
453
+ port: closestPort || '',
454
+ });
455
+ }
456
+
457
+ return { services };
458
+ }
459
+
460
+ export default RemoteXpcConnection;
461
+ export { type Service, type ServicesResponse };