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.
- package/.github/dependabot.yml +38 -0
- package/.github/workflows/format-check.yml +43 -0
- package/.github/workflows/lint-and-build.yml +40 -0
- package/.github/workflows/pr-title.yml +16 -0
- package/.github/workflows/publish.js.yml +42 -0
- package/.github/workflows/test-validation.yml +40 -0
- package/.mocharc.json +8 -0
- package/.prettierignore +3 -0
- package/.prettierrc +17 -0
- package/.releaserc +37 -0
- package/CHANGELOG.md +63 -0
- package/LICENSE +201 -0
- package/README.md +178 -0
- package/assets/images/ios-arch.png +0 -0
- package/eslint.config.js +45 -0
- package/package.json +78 -0
- package/scripts/test-tunnel-creation.ts +378 -0
- package/src/base-plist-service.ts +83 -0
- package/src/base-socket-service.ts +55 -0
- package/src/index.ts +34 -0
- package/src/lib/apple-tv/constants.ts +83 -0
- package/src/lib/apple-tv/errors.ts +31 -0
- package/src/lib/apple-tv/tlv/decoder.ts +68 -0
- package/src/lib/apple-tv/tlv/encoder.ts +33 -0
- package/src/lib/apple-tv/tlv/index.ts +6 -0
- package/src/lib/apple-tv/tlv/pairing-tlv.ts +31 -0
- package/src/lib/apple-tv/types.ts +58 -0
- package/src/lib/apple-tv/utils/buffer-utils.ts +90 -0
- package/src/lib/apple-tv/utils/index.ts +2 -0
- package/src/lib/apple-tv/utils/uuid-generator.ts +43 -0
- package/src/lib/lockdown/index.ts +468 -0
- package/src/lib/pair-record/index.ts +8 -0
- package/src/lib/pair-record/pair-record.ts +133 -0
- package/src/lib/plist/binary-plist-creator.ts +571 -0
- package/src/lib/plist/binary-plist-parser.ts +587 -0
- package/src/lib/plist/constants.ts +53 -0
- package/src/lib/plist/index.ts +54 -0
- package/src/lib/plist/length-based-splitter.ts +326 -0
- package/src/lib/plist/plist-creator.ts +42 -0
- package/src/lib/plist/plist-decoder.ts +135 -0
- package/src/lib/plist/plist-encoder.ts +36 -0
- package/src/lib/plist/plist-parser.ts +144 -0
- package/src/lib/plist/plist-service.ts +231 -0
- package/src/lib/plist/unified-plist-creator.ts +19 -0
- package/src/lib/plist/unified-plist-parser.ts +25 -0
- package/src/lib/plist/utils.ts +376 -0
- package/src/lib/remote-xpc/constants.ts +22 -0
- package/src/lib/remote-xpc/handshake-frames.ts +377 -0
- package/src/lib/remote-xpc/handshake.ts +152 -0
- package/src/lib/remote-xpc/remote-xpc-connection.ts +461 -0
- package/src/lib/remote-xpc/xpc-protocol.ts +412 -0
- package/src/lib/tunnel/index.ts +253 -0
- package/src/lib/tunnel/packet-stream-client.ts +185 -0
- package/src/lib/tunnel/packet-stream-server.ts +133 -0
- package/src/lib/tunnel/tunnel-api-client.ts +234 -0
- package/src/lib/tunnel/tunnel-registry-server.ts +410 -0
- package/src/lib/types.ts +291 -0
- package/src/lib/usbmux/index.ts +630 -0
- package/src/lib/usbmux/usbmux-decoder.ts +66 -0
- package/src/lib/usbmux/usbmux-encoder.ts +55 -0
- package/src/service-connection.ts +79 -0
- package/src/services/index.ts +15 -0
- package/src/services/ios/base-service.ts +81 -0
- package/src/services/ios/diagnostic-service/index.ts +241 -0
- package/src/services/ios/diagnostic-service/keys.ts +770 -0
- package/src/services/ios/syslog-service/index.ts +387 -0
- package/src/services/ios/tunnel-service/index.ts +88 -0
- package/src/services.ts +81 -0
- package/test/integration/diagnostics-test.ts +44 -0
- package/test/integration/read-pair-record-test.ts +39 -0
- package/test/integration/tunnel-test.ts +104 -0
- package/test/unit/apple-tv/tlv/decoder.spec.ts +144 -0
- package/test/unit/apple-tv/tlv/encoder.spec.ts +91 -0
- package/test/unit/apple-tv/tlv/pairing-tlv.spec.ts +101 -0
- package/test/unit/apple-tv/tlv/tlv-integration.spec.ts +146 -0
- package/test/unit/apple-tv/utils/buffer-utils.spec.ts +74 -0
- package/test/unit/apple-tv/utils/uuid-generator.spec.ts +39 -0
- package/test/unit/fixtures/index.ts +88 -0
- package/test/unit/fixtures/usbmuxconnectmessage.bin +0 -0
- package/test/unit/fixtures/usbmuxlistdevicemessage.bin +0 -0
- package/test/unit/plist/error-handling.spec.ts +101 -0
- package/test/unit/plist/fixtures/sample.binary.plist +0 -0
- package/test/unit/plist/fixtures/sample.xml.plist +38 -0
- package/test/unit/plist/plist-parser.spec.ts +283 -0
- package/test/unit/plist/plist.spec.ts +205 -0
- package/test/unit/plist/tag-position-handling.spec.ts +90 -0
- package/test/unit/plist/unified-plist-parser.spec.ts +227 -0
- package/test/unit/plist/utils.spec.ts +249 -0
- package/test/unit/plist/xml-cleaning.spec.ts +60 -0
- package/test/unit/tunnel/tunnel-registry-server.spec.ts +194 -0
- package/test/unit/usbmux/usbmux-specs.ts +71 -0
- package/tsconfig.json +36 -0
|
@@ -0,0 +1,387 @@
|
|
|
1
|
+
import { logger } from '@appium/support';
|
|
2
|
+
import { EventEmitter } from 'events';
|
|
3
|
+
import type { PacketConsumer, PacketData } from 'tuntap-bridge';
|
|
4
|
+
|
|
5
|
+
import { isBinaryPlist } from '../../../lib/plist/binary-plist-parser.js';
|
|
6
|
+
import { parsePlist } from '../../../lib/plist/unified-plist-parser.js';
|
|
7
|
+
import type {
|
|
8
|
+
PacketSource,
|
|
9
|
+
SyslogOptions,
|
|
10
|
+
SyslogService as SyslogServiceInterface,
|
|
11
|
+
} from '../../../lib/types.js';
|
|
12
|
+
import { ServiceConnection } from '../../../service-connection.js';
|
|
13
|
+
import { BaseService, type Service } from '../base-service.js';
|
|
14
|
+
|
|
15
|
+
const syslogLog = logger.getLogger('SyslogMessages');
|
|
16
|
+
const log = logger.getLogger('Syslog');
|
|
17
|
+
|
|
18
|
+
const MIN_PRINTABLE_RATIO = 0.5;
|
|
19
|
+
const ASCII_PRINTABLE_MIN = 32;
|
|
20
|
+
const ASCII_PRINTABLE_MAX = 126;
|
|
21
|
+
const NON_PRINTABLE_ASCII_REGEX = /[^\x20-\x7E]/g;
|
|
22
|
+
const PLIST_XML_MARKERS = ['<?xml', '<plist'];
|
|
23
|
+
const BINARY_PLIST_MARKER = 'bplist';
|
|
24
|
+
const BINARY_PLIST_MARKER_ALT = 'Ibplist00';
|
|
25
|
+
const MIN_PLIST_SIZE = 8;
|
|
26
|
+
const PLIST_HEADER_CHECK_SIZE = 100;
|
|
27
|
+
|
|
28
|
+
const DEFAULT_SYSLOG_REQUEST = {
|
|
29
|
+
Request: 'StartActivity',
|
|
30
|
+
MessageFilter: 65535,
|
|
31
|
+
StreamFlags: 60,
|
|
32
|
+
} as const;
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* syslog-service provides functionality to capture and process syslog messages
|
|
36
|
+
* from a remote device using Apple's XPC services.
|
|
37
|
+
*/
|
|
38
|
+
class SyslogService extends EventEmitter implements SyslogServiceInterface {
|
|
39
|
+
private readonly baseService: BaseService;
|
|
40
|
+
private connection: ServiceConnection | null = null;
|
|
41
|
+
private packetConsumer: PacketConsumer | null = null;
|
|
42
|
+
private packetStreamPromise: Promise<void> | null = null;
|
|
43
|
+
private isCapturing = false;
|
|
44
|
+
private enableVerboseLogging = false;
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Creates a new syslog-service instance
|
|
48
|
+
* @param address Tuple containing [host, port]
|
|
49
|
+
*/
|
|
50
|
+
constructor(address: [string, number]) {
|
|
51
|
+
super();
|
|
52
|
+
this.baseService = new BaseService(address);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Starts capturing syslog data from the device
|
|
57
|
+
* @param service Service information
|
|
58
|
+
* @param packetSource Source of packet data (can be PacketConsumer or AsyncIterable)
|
|
59
|
+
* @param options Configuration options for syslog capture
|
|
60
|
+
* @returns Promise resolving to the initial response from the service
|
|
61
|
+
*/
|
|
62
|
+
async start(
|
|
63
|
+
service: Service,
|
|
64
|
+
packetSource: PacketSource | AsyncIterable<PacketData>,
|
|
65
|
+
options: SyslogOptions = {},
|
|
66
|
+
): Promise<void> {
|
|
67
|
+
if (this.isCapturing) {
|
|
68
|
+
log.info(
|
|
69
|
+
'Syslog capture already in progress. Stopping previous capture.',
|
|
70
|
+
);
|
|
71
|
+
await this.stop();
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const { pid = -1, enableVerboseLogging = false } = options;
|
|
75
|
+
this.enableVerboseLogging = enableVerboseLogging;
|
|
76
|
+
this.isCapturing = true;
|
|
77
|
+
|
|
78
|
+
this.attachPacketSource(packetSource);
|
|
79
|
+
|
|
80
|
+
try {
|
|
81
|
+
this.connection = await this.baseService.startLockdownService(service);
|
|
82
|
+
|
|
83
|
+
const request = {
|
|
84
|
+
...DEFAULT_SYSLOG_REQUEST,
|
|
85
|
+
Pid: pid,
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
const response = await this.connection.sendPlistRequest(request);
|
|
89
|
+
log.info(`Syslog capture started: ${response}`);
|
|
90
|
+
this.emit('start', response);
|
|
91
|
+
} catch (error) {
|
|
92
|
+
this.isCapturing = false;
|
|
93
|
+
this.detachPacketSource();
|
|
94
|
+
throw error;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Stops capturing syslog data
|
|
100
|
+
* @returns Promise that resolves when capture is stopped
|
|
101
|
+
*/
|
|
102
|
+
async stop(): Promise<void> {
|
|
103
|
+
if (!this.isCapturing) {
|
|
104
|
+
log.info('No syslog capture in progress.');
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
this.detachPacketSource();
|
|
109
|
+
this.closeConnection();
|
|
110
|
+
|
|
111
|
+
this.isCapturing = false;
|
|
112
|
+
log.info('Syslog capture stopped');
|
|
113
|
+
this.emit('stop');
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Restart the device
|
|
118
|
+
* @param service Service information
|
|
119
|
+
* @returns Promise that resolves when the restart request is sent
|
|
120
|
+
*/
|
|
121
|
+
async restart(service: Service): Promise<void> {
|
|
122
|
+
try {
|
|
123
|
+
const conn = await this.baseService.startLockdownService(service);
|
|
124
|
+
const request = { Request: 'Restart' };
|
|
125
|
+
const res = await conn.sendPlistRequest(request);
|
|
126
|
+
log.info(`Restart response: ${res}`);
|
|
127
|
+
} catch (error) {
|
|
128
|
+
log.error(`Error during restart: ${error}`);
|
|
129
|
+
throw error;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
private attachPacketSource(
|
|
134
|
+
packetSource: PacketSource | AsyncIterable<PacketData>,
|
|
135
|
+
): void {
|
|
136
|
+
if (this.isPacketSource(packetSource)) {
|
|
137
|
+
this.packetConsumer = {
|
|
138
|
+
onPacket: (packet: PacketData) => this.processPacket(packet),
|
|
139
|
+
};
|
|
140
|
+
packetSource.addPacketConsumer(this.packetConsumer);
|
|
141
|
+
} else {
|
|
142
|
+
// Store the promise so we can handle it properly
|
|
143
|
+
this.packetStreamPromise = this.processPacketStream(packetSource);
|
|
144
|
+
|
|
145
|
+
// Handle any errors from the stream processing
|
|
146
|
+
this.packetStreamPromise.catch((error) => {
|
|
147
|
+
log.error(`Packet stream processing failed: ${error}`);
|
|
148
|
+
this.emit('error', error);
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
private isPacketSource(source: unknown): source is PacketSource {
|
|
154
|
+
return (
|
|
155
|
+
typeof source === 'object' &&
|
|
156
|
+
source !== null &&
|
|
157
|
+
'addPacketConsumer' in source &&
|
|
158
|
+
'removePacketConsumer' in source
|
|
159
|
+
);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
private async processPacketStream(
|
|
163
|
+
packetStream: AsyncIterable<PacketData>,
|
|
164
|
+
): Promise<void> {
|
|
165
|
+
try {
|
|
166
|
+
for await (const packet of packetStream) {
|
|
167
|
+
if (!this.isCapturing) {
|
|
168
|
+
break;
|
|
169
|
+
}
|
|
170
|
+
this.processPacket(packet);
|
|
171
|
+
}
|
|
172
|
+
} catch (error) {
|
|
173
|
+
log.error(`Error processing packet stream: ${error}`);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
private processPacket(packet: PacketData): void {
|
|
178
|
+
if (packet.protocol === 'TCP') {
|
|
179
|
+
this.processTcpPacket(packet);
|
|
180
|
+
} else if (packet.protocol === 'UDP') {
|
|
181
|
+
this.processUdpPacket(packet);
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Detaches the packet source
|
|
187
|
+
*/
|
|
188
|
+
private detachPacketSource(): void {
|
|
189
|
+
if (this.packetConsumer) {
|
|
190
|
+
this.packetConsumer = null;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// Cancel the packet stream processing if it's running
|
|
194
|
+
if (this.packetStreamPromise) {
|
|
195
|
+
// Setting isCapturing to false will cause the stream loop to exit
|
|
196
|
+
this.packetStreamPromise = null;
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* Closes the current connection
|
|
202
|
+
*/
|
|
203
|
+
private closeConnection(): void {
|
|
204
|
+
if (!this.connection) {
|
|
205
|
+
return;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
try {
|
|
209
|
+
this.connection.close();
|
|
210
|
+
} catch (error) {
|
|
211
|
+
log.debug(`Error closing connection: ${error}`);
|
|
212
|
+
} finally {
|
|
213
|
+
this.connection = null;
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* Processes a TCP packet
|
|
219
|
+
* @param packet TCP packet to process
|
|
220
|
+
*/
|
|
221
|
+
private processTcpPacket(packet: PacketData): void {
|
|
222
|
+
try {
|
|
223
|
+
if (this.mightBePlist(packet.payload)) {
|
|
224
|
+
this.processPlistPacket(packet);
|
|
225
|
+
} else {
|
|
226
|
+
this.processTextPacket(packet);
|
|
227
|
+
}
|
|
228
|
+
} catch (error) {
|
|
229
|
+
log.debug(`Error processing packet: ${error}`);
|
|
230
|
+
this.emitTextMessage(packet.payload);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
this.logPacketDetails(packet);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
private processPlistPacket(packet: PacketData): void {
|
|
237
|
+
try {
|
|
238
|
+
const plistData = parsePlist(packet.payload);
|
|
239
|
+
log.debug('Successfully parsed packet as plist');
|
|
240
|
+
this.emit('plist', plistData);
|
|
241
|
+
|
|
242
|
+
const message = JSON.stringify(plistData);
|
|
243
|
+
this.emitMessage(message);
|
|
244
|
+
} catch (error) {
|
|
245
|
+
log.debug(`Failed to parse as plist: ${error}`);
|
|
246
|
+
this.processTextPacket(packet);
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
private processTextPacket(packet: PacketData): void {
|
|
251
|
+
const message = this.extractPrintableText(packet.payload);
|
|
252
|
+
if (!message.trim()) {
|
|
253
|
+
log.debug('TCP packet contains no printable text, ignoring.');
|
|
254
|
+
return;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
const isMostlyPrintable = this.isMostlyPrintable(packet.payload);
|
|
258
|
+
if (!isMostlyPrintable) {
|
|
259
|
+
log.debug(
|
|
260
|
+
`TCP packet not mostly printable, but contains text: ${message}`,
|
|
261
|
+
);
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
this.emitMessage(message);
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
private emitTextMessage(buffer: Buffer): void {
|
|
268
|
+
const message = this.extractPrintableText(buffer);
|
|
269
|
+
if (message.trim()) {
|
|
270
|
+
this.emitMessage(message);
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
private emitMessage(message: string): void {
|
|
275
|
+
if (this.enableVerboseLogging) {
|
|
276
|
+
syslogLog.info(message);
|
|
277
|
+
}
|
|
278
|
+
this.emit('message', message);
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
/**
|
|
282
|
+
* Checks if the buffer might be a plist (XML or binary)
|
|
283
|
+
* @param buffer Buffer to check
|
|
284
|
+
* @returns True if the buffer might be a plist
|
|
285
|
+
*/
|
|
286
|
+
private mightBePlist(buffer: Buffer): boolean {
|
|
287
|
+
try {
|
|
288
|
+
if (buffer.length < MIN_PLIST_SIZE) {
|
|
289
|
+
return false;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
// Check for XML plist
|
|
293
|
+
const headerStr = buffer.toString(
|
|
294
|
+
'utf8',
|
|
295
|
+
0,
|
|
296
|
+
Math.min(PLIST_HEADER_CHECK_SIZE, buffer.length),
|
|
297
|
+
);
|
|
298
|
+
if (PLIST_XML_MARKERS.every((marker) => headerStr.includes(marker))) {
|
|
299
|
+
return true;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
// Check for binary plist
|
|
303
|
+
if (isBinaryPlist(buffer)) {
|
|
304
|
+
return true;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
// Check alternative binary plist markers
|
|
308
|
+
const firstNineChars = buffer.toString(
|
|
309
|
+
'ascii',
|
|
310
|
+
0,
|
|
311
|
+
Math.min(9, buffer.length),
|
|
312
|
+
);
|
|
313
|
+
return (
|
|
314
|
+
firstNineChars === BINARY_PLIST_MARKER_ALT ||
|
|
315
|
+
firstNineChars.includes(BINARY_PLIST_MARKER)
|
|
316
|
+
);
|
|
317
|
+
} catch (error) {
|
|
318
|
+
log.debug(`Error checking if buffer is plist: ${error}`);
|
|
319
|
+
return false;
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
/**
|
|
324
|
+
* Processes a UDP packet
|
|
325
|
+
* @param packet UDP packet to process
|
|
326
|
+
*/
|
|
327
|
+
private processUdpPacket(packet: PacketData): void {
|
|
328
|
+
log.debug(`Received UDP packet (not filtered here): ${packet}`);
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
/**
|
|
332
|
+
* Logs packet details for debugging
|
|
333
|
+
* @param packet Packet to log details for
|
|
334
|
+
*/
|
|
335
|
+
private logPacketDetails(packet: PacketData): void {
|
|
336
|
+
log.debug('Received syslog-like TCP packet:');
|
|
337
|
+
log.debug(` Source: ${packet.src}`);
|
|
338
|
+
log.debug(` Destination: ${packet.dst}`);
|
|
339
|
+
log.debug(` Source port: ${packet.sourcePort}`);
|
|
340
|
+
log.debug(` Destination port: ${packet.destPort}`);
|
|
341
|
+
log.debug(` Payload length: ${packet.payload.length}`);
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
/**
|
|
345
|
+
* Extracts printable text from a buffer
|
|
346
|
+
* @param buffer Buffer to extract text from
|
|
347
|
+
* @returns Printable text
|
|
348
|
+
*/
|
|
349
|
+
private extractPrintableText(buffer: Buffer): string {
|
|
350
|
+
return buffer.toString().replace(NON_PRINTABLE_ASCII_REGEX, '');
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
/**
|
|
354
|
+
* Determines if a buffer contains mostly printable ASCII characters
|
|
355
|
+
* @param buffer Buffer to analyze
|
|
356
|
+
* @returns True if more than 50% of characters are printable ASCII
|
|
357
|
+
*/
|
|
358
|
+
private isMostlyPrintable(buffer: Buffer): boolean {
|
|
359
|
+
try {
|
|
360
|
+
const str = buffer.toString('utf8');
|
|
361
|
+
if (!str || str.length === 0) {
|
|
362
|
+
return false;
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
const totalLength = str.length;
|
|
366
|
+
const threshold = totalLength * MIN_PRINTABLE_RATIO;
|
|
367
|
+
let printableCount = 0;
|
|
368
|
+
|
|
369
|
+
for (let i = 0; i < totalLength; i++) {
|
|
370
|
+
const code = str.charCodeAt(i);
|
|
371
|
+
if (code >= ASCII_PRINTABLE_MIN && code <= ASCII_PRINTABLE_MAX) {
|
|
372
|
+
printableCount++;
|
|
373
|
+
if (printableCount > threshold) {
|
|
374
|
+
return true;
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
return printableCount / totalLength > MIN_PRINTABLE_RATIO;
|
|
380
|
+
} catch (error) {
|
|
381
|
+
log.debug(error);
|
|
382
|
+
return false;
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
export default SyslogService;
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { logger } from '@appium/support';
|
|
2
|
+
import { TLSSocket } from 'tls';
|
|
3
|
+
|
|
4
|
+
import {
|
|
5
|
+
LockdownService,
|
|
6
|
+
upgradeSocketToTLS,
|
|
7
|
+
} from '../../../lib/lockdown/index.js';
|
|
8
|
+
import { PlistService } from '../../../lib/plist/plist-service.js';
|
|
9
|
+
import { createUsbmux } from '../../../lib/usbmux/index.js';
|
|
10
|
+
|
|
11
|
+
const log = logger.getLogger('TunnelService');
|
|
12
|
+
const LABEL = 'appium-internal';
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Starts a CoreDeviceProxy session over an existing TLS-upgraded lockdown connection.
|
|
16
|
+
*
|
|
17
|
+
* @param lockdownClient - The TLS-upgraded lockdown client used to send the StartService request.
|
|
18
|
+
* @param deviceID - The device identifier to be used in the Connect request.
|
|
19
|
+
* @param udid - The device UDID used to retrieve the pair record.
|
|
20
|
+
* @param tlsOptions - TLS options for upgrading the usbmuxd socket.
|
|
21
|
+
* @returns A promise that resolves with a TLS-upgraded socket and PlistService for communication with CoreDeviceProxy.
|
|
22
|
+
*/
|
|
23
|
+
export async function startCoreDeviceProxy(
|
|
24
|
+
lockdownClient: LockdownService,
|
|
25
|
+
deviceID: number | string,
|
|
26
|
+
udid: string,
|
|
27
|
+
tlsOptions: Partial<import('tls').ConnectionOptions> = {},
|
|
28
|
+
): Promise<{ socket: TLSSocket; plistService: PlistService }> {
|
|
29
|
+
// Wait for TLS upgrade to complete if in progress
|
|
30
|
+
await lockdownClient.waitForTLSUpgrade();
|
|
31
|
+
|
|
32
|
+
const response = await lockdownClient.sendAndReceive({
|
|
33
|
+
Label: LABEL,
|
|
34
|
+
Request: 'StartService',
|
|
35
|
+
Service: 'com.apple.internal.devicecompute.CoreDeviceProxy',
|
|
36
|
+
EscrowBag: null,
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
lockdownClient.close();
|
|
40
|
+
|
|
41
|
+
if (!response.Port) {
|
|
42
|
+
throw new Error('Service didnt return a port');
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
log.debug(`Connecting to CoreDeviceProxy service on port: ${response.Port}`);
|
|
46
|
+
|
|
47
|
+
const usbmux = await createUsbmux();
|
|
48
|
+
try {
|
|
49
|
+
const pairRecord = await usbmux.readPairRecord(udid);
|
|
50
|
+
if (
|
|
51
|
+
!pairRecord ||
|
|
52
|
+
!pairRecord.HostCertificate ||
|
|
53
|
+
!pairRecord.HostPrivateKey
|
|
54
|
+
) {
|
|
55
|
+
throw new Error(
|
|
56
|
+
'Missing required pair record or certificates for TLS upgrade',
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const coreDeviceSocket = await usbmux.connect(
|
|
61
|
+
Number(deviceID),
|
|
62
|
+
Number(response.Port),
|
|
63
|
+
);
|
|
64
|
+
|
|
65
|
+
log.debug('Socket connected to CoreDeviceProxy, upgrading to TLS...');
|
|
66
|
+
|
|
67
|
+
const fullTlsOptions = {
|
|
68
|
+
...tlsOptions,
|
|
69
|
+
cert: pairRecord.HostCertificate,
|
|
70
|
+
key: pairRecord.HostPrivateKey,
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
const tlsSocket = await upgradeSocketToTLS(
|
|
74
|
+
coreDeviceSocket,
|
|
75
|
+
fullTlsOptions,
|
|
76
|
+
);
|
|
77
|
+
|
|
78
|
+
const plistService = new PlistService(tlsSocket);
|
|
79
|
+
|
|
80
|
+
return { socket: tlsSocket, plistService };
|
|
81
|
+
} catch (err) {
|
|
82
|
+
// If we haven't connected yet, we can safely close the usbmux
|
|
83
|
+
await usbmux
|
|
84
|
+
.close()
|
|
85
|
+
.catch((closeErr) => log.error(`Error closing usbmux: ${closeErr}`));
|
|
86
|
+
throw err;
|
|
87
|
+
}
|
|
88
|
+
}
|
package/src/services.ts
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { strongbox } from '@appium/strongbox';
|
|
2
|
+
|
|
3
|
+
import RemoteXpcConnection from './lib/remote-xpc/remote-xpc-connection.js';
|
|
4
|
+
import { TunnelManager } from './lib/tunnel/index.js';
|
|
5
|
+
import { TunnelApiClient } from './lib/tunnel/tunnel-api-client.js';
|
|
6
|
+
import type {
|
|
7
|
+
DiagnosticsService as DiagnosticsServiceType,
|
|
8
|
+
SyslogService as SyslogServiceType,
|
|
9
|
+
} from './lib/types.js';
|
|
10
|
+
import DiagnosticsService from './services/ios/diagnostic-service/index.js';
|
|
11
|
+
import SyslogService from './services/ios/syslog-service/index.js';
|
|
12
|
+
|
|
13
|
+
const APPIUM_XCUITEST_DRIVER_NAME = 'appium-xcuitest-driver';
|
|
14
|
+
const TUNNEL_REGISTRY_PORT = 'tunnelRegistryPort';
|
|
15
|
+
|
|
16
|
+
export async function startDiagnosticsService(
|
|
17
|
+
udid: string,
|
|
18
|
+
): Promise<DiagnosticsServiceType> {
|
|
19
|
+
const { remoteXPC, tunnelConnection } = await createRemoteXPCConnection(udid);
|
|
20
|
+
const diagnosticsService = remoteXPC.findService(
|
|
21
|
+
DiagnosticsService.RSD_SERVICE_NAME,
|
|
22
|
+
);
|
|
23
|
+
return new DiagnosticsService([
|
|
24
|
+
tunnelConnection.host,
|
|
25
|
+
parseInt(diagnosticsService.port, 10),
|
|
26
|
+
]);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export async function startSyslogService(
|
|
30
|
+
udid: string,
|
|
31
|
+
): Promise<SyslogServiceType> {
|
|
32
|
+
const { tunnelConnection } = await createRemoteXPCConnection(udid);
|
|
33
|
+
return new SyslogService([tunnelConnection.host, tunnelConnection.port]);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export async function createRemoteXPCConnection(udid: string) {
|
|
37
|
+
const tunnelConnection = await getTunnelInformation(udid);
|
|
38
|
+
const remoteXPC = await startService(
|
|
39
|
+
tunnelConnection.host,
|
|
40
|
+
tunnelConnection.port,
|
|
41
|
+
);
|
|
42
|
+
return { remoteXPC, tunnelConnection };
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// #region Private Functions
|
|
46
|
+
|
|
47
|
+
async function getTunnelInformation(udid: string) {
|
|
48
|
+
const box = strongbox(APPIUM_XCUITEST_DRIVER_NAME);
|
|
49
|
+
const item = await box.createItem(TUNNEL_REGISTRY_PORT);
|
|
50
|
+
const tunnelRegistryPort = await item.read();
|
|
51
|
+
if (tunnelRegistryPort === undefined) {
|
|
52
|
+
throw new Error(
|
|
53
|
+
'Tunnel registry port not found. Please run the tunnel creation script first: sudo appium driver run xcuitest tunnel-creation',
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
const tunnelApiClient = new TunnelApiClient(
|
|
57
|
+
`http://127.0.0.1:${tunnelRegistryPort}/remotexpc/tunnels`,
|
|
58
|
+
);
|
|
59
|
+
const tunnelExists = await tunnelApiClient.hasTunnel(udid);
|
|
60
|
+
if (!tunnelExists) {
|
|
61
|
+
throw new Error(
|
|
62
|
+
`No tunnel found for device ${udid}. Please run the tunnel creation script first: sudo appium driver run xcuitest tunnel-creation`,
|
|
63
|
+
);
|
|
64
|
+
}
|
|
65
|
+
const tunnelConnection = await tunnelApiClient.getTunnelConnection(udid);
|
|
66
|
+
if (!tunnelConnection) {
|
|
67
|
+
throw new Error(
|
|
68
|
+
`Failed to get tunnel connection details for device ${udid}`,
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
return tunnelConnection;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
async function startService(
|
|
75
|
+
host: string,
|
|
76
|
+
port: number,
|
|
77
|
+
): Promise<RemoteXpcConnection> {
|
|
78
|
+
return await TunnelManager.createRemoteXPCConnection(host, port);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// #endregion
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { expect } from 'chai';
|
|
2
|
+
|
|
3
|
+
import { Services } from '../../src/index.js';
|
|
4
|
+
import type { DiagnosticsService } from '../../src/lib/types.js';
|
|
5
|
+
|
|
6
|
+
describe('Diagnostics Service', function () {
|
|
7
|
+
// Increase timeout for integration tests
|
|
8
|
+
this.timeout(60000);
|
|
9
|
+
|
|
10
|
+
let remoteXPC: any;
|
|
11
|
+
let diagService: DiagnosticsService;
|
|
12
|
+
const udid = process.env.UDID || '';
|
|
13
|
+
|
|
14
|
+
before(async function () {
|
|
15
|
+
diagService = await Services.startDiagnosticsService(udid);
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
after(async function () {
|
|
19
|
+
// Close RemoteXPC connection
|
|
20
|
+
if (remoteXPC) {
|
|
21
|
+
try {
|
|
22
|
+
await remoteXPC.close();
|
|
23
|
+
} catch (error) {
|
|
24
|
+
// Ignore cleanup errors in tests
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it('should query power information using ioregistry', async function () {
|
|
30
|
+
const rawInfo = await diagService.ioregistry({
|
|
31
|
+
ioClass: 'IOPMPowerSource',
|
|
32
|
+
returnRawJson: true,
|
|
33
|
+
});
|
|
34
|
+
expect(rawInfo).to.be.an('object');
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it('should query wifi information using ioregistry ', async function () {
|
|
38
|
+
const wifiInfo = await diagService.ioregistry({
|
|
39
|
+
name: 'AppleBCMWLANSkywalkInterface',
|
|
40
|
+
returnRawJson: true,
|
|
41
|
+
});
|
|
42
|
+
expect(wifiInfo).to.be.an('object');
|
|
43
|
+
});
|
|
44
|
+
});
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { expect } from 'chai';
|
|
2
|
+
|
|
3
|
+
import { createUsbmux } from '../../src/lib/usbmux/index.js';
|
|
4
|
+
|
|
5
|
+
describe('Pair Record', function () {
|
|
6
|
+
// Increase timeout for integration tests
|
|
7
|
+
this.timeout(60000);
|
|
8
|
+
|
|
9
|
+
let usb: any;
|
|
10
|
+
|
|
11
|
+
before(async function () {
|
|
12
|
+
usb = await createUsbmux();
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
after(async function () {
|
|
16
|
+
if (usb) {
|
|
17
|
+
await usb.close();
|
|
18
|
+
}
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it('should read pair record', async function () {
|
|
22
|
+
try {
|
|
23
|
+
await usb.readPairRecord('');
|
|
24
|
+
// If no error is thrown, the test passes
|
|
25
|
+
expect(true).to.be.true;
|
|
26
|
+
} catch (err) {
|
|
27
|
+
console.log(err);
|
|
28
|
+
// If the error is expected (e.g., no pair record found), the test can still pass
|
|
29
|
+
// Otherwise, fail the test
|
|
30
|
+
expect(err).to.not.be.undefined;
|
|
31
|
+
}
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it('should list devices', async function () {
|
|
35
|
+
const devices = await usb.listDevices();
|
|
36
|
+
console.log(devices);
|
|
37
|
+
expect(devices).to.be.an('array');
|
|
38
|
+
});
|
|
39
|
+
});
|