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,55 @@
1
+ import { Transform, type TransformCallback } from 'stream';
2
+
3
+ import { createPlist } from '../plist/index.js';
4
+ import type { PlistDictionary } from '../types.js';
5
+
6
+ const HEADER_LENGTH = 16;
7
+ const VERSION = 1;
8
+ const TYPE = 8;
9
+
10
+ export interface UsbmuxEncodeData {
11
+ payload: PlistDictionary; // Using PlistDictionary for the payload
12
+ tag: number;
13
+ }
14
+
15
+ export class UsbmuxEncoder extends Transform {
16
+ constructor() {
17
+ super({ objectMode: true });
18
+ }
19
+
20
+ _transform(
21
+ data: UsbmuxEncodeData,
22
+ encoding: BufferEncoding,
23
+ callback: TransformCallback,
24
+ ): void {
25
+ this._encode(data);
26
+ callback();
27
+ }
28
+
29
+ private _encode(data: UsbmuxEncodeData): void {
30
+ const plistData = createPlist(data.payload, false);
31
+ const payloadBuffer = Buffer.isBuffer(plistData)
32
+ ? plistData
33
+ : Buffer.from(plistData);
34
+
35
+ const header = {
36
+ length: HEADER_LENGTH + payloadBuffer.length,
37
+ version: VERSION,
38
+ type: TYPE,
39
+ tag: data.tag,
40
+ };
41
+
42
+ const headerBuffer = Buffer.allocUnsafe(HEADER_LENGTH);
43
+ headerBuffer.writeUInt32LE(header.length, 0);
44
+ headerBuffer.writeUInt32LE(header.version, 4);
45
+ headerBuffer.writeUInt32LE(header.type, 8);
46
+ headerBuffer.writeUInt32LE(header.tag, 12);
47
+
48
+ this.push(
49
+ Buffer.concat(
50
+ [headerBuffer, payloadBuffer],
51
+ headerBuffer.length + payloadBuffer.length,
52
+ ),
53
+ );
54
+ }
55
+ }
@@ -0,0 +1,79 @@
1
+ import net from 'node:net';
2
+
3
+ import { BasePlistService } from './base-plist-service.js';
4
+ import type { PlistServiceOptions } from './lib/plist/plist-service.js';
5
+ import type { PlistDictionary } from './lib/types.js';
6
+
7
+ export interface ServiceConnectionOptions {
8
+ keepAlive?: boolean;
9
+ createConnectionTimeout?: number;
10
+ plistOptions?: PlistServiceOptions;
11
+ }
12
+
13
+ /**
14
+ * ServiceConnection for communicating with Apple device services over TCP
15
+ */
16
+ export class ServiceConnection extends BasePlistService {
17
+ constructor(socket: net.Socket, options?: ServiceConnectionOptions) {
18
+ super(socket, options?.plistOptions);
19
+ }
20
+
21
+ /**
22
+ * Creates a TCP connection to the specified host and port
23
+ */
24
+ static createUsingTCP(
25
+ hostname: string,
26
+ port: string,
27
+ options?: ServiceConnectionOptions,
28
+ ): Promise<ServiceConnection> {
29
+ const keepAlive = options?.keepAlive ?? true;
30
+ const createConnectionTimeout = options?.createConnectionTimeout ?? 30000;
31
+
32
+ return new Promise<ServiceConnection>((resolve, reject) => {
33
+ const socket = net.createConnection(
34
+ { host: hostname, port: Number(port) },
35
+ () => {
36
+ socket.setTimeout(0);
37
+ if (keepAlive) {
38
+ socket.setKeepAlive(true);
39
+ }
40
+ resolve(new ServiceConnection(socket, options));
41
+ },
42
+ );
43
+
44
+ socket.setTimeout(createConnectionTimeout, () => {
45
+ socket.destroy();
46
+ reject(new Error('Connection timed out'));
47
+ });
48
+
49
+ socket.on('error', (err: Error) => reject(err));
50
+ });
51
+ }
52
+
53
+ /**
54
+ * Sends a plist request to the device and returns the response
55
+ */
56
+ sendPlistRequest(
57
+ requestObj: PlistDictionary,
58
+ timeout = 10000,
59
+ ): Promise<PlistDictionary> {
60
+ return this.sendAndReceive(requestObj, timeout);
61
+ }
62
+
63
+ /**
64
+ * Gets the underlying socket
65
+ * @returns The socket used by this service
66
+ */
67
+ getSocket(): net.Socket {
68
+ return this.getPlistService().getSocket() as net.Socket;
69
+ }
70
+
71
+ /**
72
+ * Closes the connection
73
+ */
74
+ close(): void {
75
+ super.close();
76
+ }
77
+ }
78
+
79
+ export default ServiceConnection;
@@ -0,0 +1,15 @@
1
+ import {
2
+ TunnelRegistryServer,
3
+ startTunnelRegistryServer,
4
+ } from '../lib/tunnel/tunnel-registry-server.js';
5
+ import * as diagnostics from './ios/diagnostic-service/index.js';
6
+ import * as syslog from './ios/syslog-service/index.js';
7
+ import * as tunnel from './ios/tunnel-service/index.js';
8
+
9
+ export {
10
+ diagnostics,
11
+ syslog,
12
+ tunnel,
13
+ TunnelRegistryServer,
14
+ startTunnelRegistryServer,
15
+ };
@@ -0,0 +1,81 @@
1
+ import { logger } from '@appium/support';
2
+
3
+ import { ServiceConnection } from '../../service-connection.js';
4
+
5
+ const log = logger.getLogger('BaseService');
6
+
7
+ /**
8
+ * Interface for service information
9
+ */
10
+ export interface Service {
11
+ serviceName: string;
12
+ port: string;
13
+ }
14
+
15
+ /**
16
+ * Base class for iOS services that provides common functionality
17
+ */
18
+ export class BaseService {
19
+ protected readonly address: [string, number];
20
+
21
+ /**
22
+ * Creates a new BaseService instance
23
+ * @param address Tuple containing [host, port]
24
+ */
25
+ constructor(address: [string, number]) {
26
+ this.address = address;
27
+ }
28
+
29
+ /**
30
+ * Starts a lockdown service without sending a check-in message
31
+ * @param service Service information
32
+ * @param options Additional options for the connection
33
+ * @returns Promise resolving to a ServiceConnection
34
+ */
35
+ public async startLockdownWithoutCheckin(
36
+ service: Service,
37
+ options: Record<string, any> = {},
38
+ ): Promise<ServiceConnection> {
39
+ // Get the port for the requested service
40
+ const port = service.port;
41
+ return ServiceConnection.createUsingTCP(this.address[0], port, options);
42
+ }
43
+
44
+ /**
45
+ * Starts a lockdown service with proper check-in
46
+ * @param service Service information
47
+ * @param options Additional options for the connection
48
+ * @returns Promise resolving to a ServiceConnection
49
+ */
50
+ public async startLockdownService(
51
+ service: Service,
52
+ options: Record<string, any> = {},
53
+ ): Promise<ServiceConnection> {
54
+ try {
55
+ const connection = await this.startLockdownWithoutCheckin(
56
+ service,
57
+ options,
58
+ );
59
+ const checkin = {
60
+ Label: 'appium-internal',
61
+ ProtocolVersion: '2',
62
+ Request: 'RSDCheckin',
63
+ };
64
+
65
+ const response = await connection.sendPlistRequest(checkin);
66
+ log.debug(
67
+ `Service check-in response: ${JSON.stringify(response, null, 2)}`,
68
+ );
69
+ return connection;
70
+ } catch (error: unknown) {
71
+ log.error('Error during check-in:', error);
72
+ if (error instanceof Error) {
73
+ log.error('Error message:', error.message);
74
+ log.error('Error stack:', error.stack);
75
+ }
76
+ throw error;
77
+ }
78
+ }
79
+ }
80
+
81
+ export default BaseService;
@@ -0,0 +1,241 @@
1
+ import { logger } from '@appium/support';
2
+
3
+ import { PlistServiceDecoder } from '../../../lib/plist/plist-decoder.js';
4
+ import type {
5
+ DiagnosticsService as DiagnosticsServiceInterface,
6
+ PlistDictionary,
7
+ } from '../../../lib/types.js';
8
+ import { BaseService } from '../base-service.js';
9
+
10
+ const log = logger.getLogger('DiagnosticService');
11
+
12
+ /**
13
+ * DiagnosticsService provides an API to:
14
+ * - Query MobileGestalt & IORegistry keys
15
+ * - Reboot, shutdown or put the device in sleep mode
16
+ * - Get WiFi information
17
+ */
18
+ class DiagnosticsService
19
+ extends BaseService
20
+ implements DiagnosticsServiceInterface
21
+ {
22
+ static readonly RSD_SERVICE_NAME =
23
+ 'com.apple.mobile.diagnostics_relay.shim.remote';
24
+
25
+ constructor(address: [string, number]) {
26
+ super(address);
27
+ }
28
+
29
+ /**
30
+ * Restart the device
31
+ * @returns Promise that resolves when the restart request is sent
32
+ */
33
+ async restart(): Promise<PlistDictionary> {
34
+ try {
35
+ const request: PlistDictionary = {
36
+ Request: 'Restart',
37
+ };
38
+
39
+ return await this.sendRequest(request);
40
+ } catch (error) {
41
+ log.error(`Error restarting device: ${error}`);
42
+ throw error;
43
+ }
44
+ }
45
+
46
+ /**
47
+ * Shutdown the device
48
+ * @returns Promise that resolves when the shutdown request is sent
49
+ */
50
+ async shutdown(): Promise<PlistDictionary> {
51
+ try {
52
+ const request: PlistDictionary = {
53
+ Request: 'Shutdown',
54
+ };
55
+
56
+ return await this.sendRequest(request);
57
+ } catch (error) {
58
+ log.error(`Error shutting down device: ${error}`);
59
+ throw error;
60
+ }
61
+ }
62
+
63
+ /**
64
+ * Put the device in sleep mode
65
+ * @returns Promise that resolves when the sleep request is sent
66
+ */
67
+ async sleep(): Promise<PlistDictionary> {
68
+ try {
69
+ const request: PlistDictionary = {
70
+ Request: 'Sleep',
71
+ };
72
+
73
+ return await this.sendRequest(request);
74
+ } catch (error) {
75
+ log.error(`Error putting device to sleep: ${error}`);
76
+ throw error;
77
+ }
78
+ }
79
+
80
+ /**
81
+ * Query IORegistry
82
+ * @returns Object containing the IORegistry information
83
+ * @param options
84
+ */
85
+ async ioregistry(options?: {
86
+ plane?: string;
87
+ name?: string;
88
+ ioClass?: string;
89
+ returnRawJson?: boolean;
90
+ timeout?: number;
91
+ }): Promise<PlistDictionary[] | Record<string, any>> {
92
+ try {
93
+ const request: PlistDictionary = {
94
+ Request: 'IORegistry',
95
+ };
96
+
97
+ if (options?.plane) {
98
+ request.CurrentPlane = options.plane;
99
+ }
100
+ if (options?.name) {
101
+ request.EntryName = options.name;
102
+ }
103
+ if (options?.ioClass) {
104
+ request.EntryClass = options.ioClass;
105
+ }
106
+
107
+ PlistServiceDecoder.lastDecodedResult = null;
108
+
109
+ const timeout = options?.timeout || 3000;
110
+
111
+ log.debug('Sending IORegistry request...');
112
+
113
+ const conn = await this.connectToDiagnosticService();
114
+ const response = await conn.sendPlistRequest(request, timeout);
115
+
116
+ log.debug(
117
+ `IORegistry response size: ${JSON.stringify(response).length} bytes`,
118
+ );
119
+
120
+ if (options?.returnRawJson) {
121
+ return await this.handleMultipartIORegistryResponse(
122
+ conn,
123
+ response,
124
+ timeout,
125
+ );
126
+ }
127
+
128
+ return this.processIORegistryResponse(response);
129
+ } catch (error) {
130
+ log.error(`Error querying IORegistry: ${error}`);
131
+ throw error;
132
+ }
133
+ }
134
+
135
+ private getServiceConfig() {
136
+ return {
137
+ serviceName: DiagnosticsService.RSD_SERVICE_NAME,
138
+ port: this.address[1].toString(),
139
+ };
140
+ }
141
+
142
+ private async connectToDiagnosticService() {
143
+ const service = this.getServiceConfig();
144
+ return await this.startLockdownService(service);
145
+ }
146
+
147
+ private async sendRequest(
148
+ request: PlistDictionary,
149
+ timeout?: number,
150
+ ): Promise<PlistDictionary> {
151
+ const conn = await this.connectToDiagnosticService();
152
+ const response = await conn.sendPlistRequest(request, timeout);
153
+
154
+ log.debug(`${request.Request} response received`);
155
+
156
+ if (!response) {
157
+ return {};
158
+ }
159
+
160
+ if (Array.isArray(response)) {
161
+ return response.length > 0 ? (response[0] as PlistDictionary) : {};
162
+ }
163
+
164
+ return response as PlistDictionary;
165
+ }
166
+
167
+ private processIORegistryResponse(
168
+ response: any,
169
+ ): PlistDictionary[] | Record<string, any> {
170
+ if (PlistServiceDecoder.lastDecodedResult) {
171
+ if (Array.isArray(PlistServiceDecoder.lastDecodedResult)) {
172
+ return PlistServiceDecoder.lastDecodedResult as PlistDictionary[];
173
+ }
174
+ return [PlistServiceDecoder.lastDecodedResult as PlistDictionary];
175
+ }
176
+
177
+ if (!response) {
178
+ throw new Error('Invalid response from IORegistry');
179
+ }
180
+
181
+ if (Array.isArray(response)) {
182
+ if (response.length === 0 && typeof response === 'object') {
183
+ log.debug('Received empty array response');
184
+ return [{ IORegistryResponse: 'No data found' } as PlistDictionary];
185
+ }
186
+ return response as PlistDictionary[];
187
+ }
188
+
189
+ if (
190
+ typeof response === 'object' &&
191
+ !Buffer.isBuffer(response) &&
192
+ !(response instanceof Date)
193
+ ) {
194
+ const responseObj = response as Record<string, any>;
195
+
196
+ if (
197
+ responseObj.Diagnostics &&
198
+ typeof responseObj.Diagnostics === 'object'
199
+ ) {
200
+ return [responseObj.Diagnostics as PlistDictionary];
201
+ }
202
+
203
+ return [responseObj as PlistDictionary];
204
+ }
205
+
206
+ return [{ value: response } as PlistDictionary];
207
+ }
208
+
209
+ private async handleMultipartIORegistryResponse(
210
+ conn: any,
211
+ initialResponse: any,
212
+ timeout: number,
213
+ ): Promise<Record<string, any>> {
214
+ await new Promise((resolve) => setTimeout(resolve, 500));
215
+
216
+ const emptyRequest: PlistDictionary = {
217
+ Request: 'Status',
218
+ };
219
+
220
+ log.debug('Sending follow-up request for additional data');
221
+
222
+ const additionalResponse = await conn.sendPlistRequest(
223
+ emptyRequest,
224
+ timeout,
225
+ );
226
+ log.debug('Additional response: ', additionalResponse);
227
+ const hasDiagnostics =
228
+ 'Diagnostics' in additionalResponse &&
229
+ typeof additionalResponse.Diagnostics === 'object' &&
230
+ additionalResponse.Diagnostics !== null &&
231
+ 'IORegistry' in additionalResponse.Diagnostics;
232
+ if (additionalResponse.Status !== 'Success' && hasDiagnostics) {
233
+ throw new Error(`Error getting diagnostic data: ${additionalResponse}`);
234
+ }
235
+
236
+ log.debug('Using additional response with IORegistry data');
237
+ return additionalResponse.Diagnostics.IORegistry as Record<string, any>;
238
+ }
239
+ }
240
+
241
+ export default DiagnosticsService;