appium-ios-remotexpc 0.13.2 → 0.15.0
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/CHANGELOG.md +12 -0
- package/build/src/index.d.ts +1 -1
- package/build/src/index.d.ts.map +1 -1
- package/build/src/lib/plist/binary-plist-creator.d.ts.map +1 -1
- package/build/src/lib/plist/binary-plist-creator.js +30 -0
- package/build/src/lib/plist/index.d.ts +1 -0
- package/build/src/lib/plist/index.d.ts.map +1 -1
- package/build/src/lib/plist/index.js +1 -0
- package/build/src/lib/plist/plist-uid.d.ts +10 -0
- package/build/src/lib/plist/plist-uid.d.ts.map +1 -0
- package/build/src/lib/plist/plist-uid.js +10 -0
- package/build/src/lib/types.d.ts +177 -2
- package/build/src/lib/types.d.ts.map +1 -1
- package/build/src/services/ios/dvt/channel-fragmenter.d.ts +21 -0
- package/build/src/services/ios/dvt/channel-fragmenter.d.ts.map +1 -0
- package/build/src/services/ios/dvt/channel-fragmenter.js +37 -0
- package/build/src/services/ios/dvt/channel.d.ts +32 -0
- package/build/src/services/ios/dvt/channel.d.ts.map +1 -0
- package/build/src/services/ios/dvt/channel.js +44 -0
- package/build/src/services/ios/dvt/dtx-message.d.ts +88 -0
- package/build/src/services/ios/dvt/dtx-message.d.ts.map +1 -0
- package/build/src/services/ios/dvt/dtx-message.js +113 -0
- package/build/src/services/ios/dvt/index.d.ts +119 -0
- package/build/src/services/ios/dvt/index.d.ts.map +1 -0
- package/build/src/services/ios/dvt/index.js +552 -0
- package/build/src/services/ios/dvt/instruments/condition-inducer.d.ts +37 -0
- package/build/src/services/ios/dvt/instruments/condition-inducer.d.ts.map +1 -0
- package/build/src/services/ios/dvt/instruments/condition-inducer.js +99 -0
- package/build/src/services/ios/dvt/instruments/location-simulation.d.ts +43 -0
- package/build/src/services/ios/dvt/instruments/location-simulation.d.ts.map +1 -0
- package/build/src/services/ios/dvt/instruments/location-simulation.js +60 -0
- package/build/src/services/ios/dvt/instruments/screenshot.d.ts +17 -0
- package/build/src/services/ios/dvt/instruments/screenshot.d.ts.map +1 -0
- package/build/src/services/ios/dvt/instruments/screenshot.js +35 -0
- package/build/src/services/ios/dvt/nskeyedarchiver-decoder.d.ts +41 -0
- package/build/src/services/ios/dvt/nskeyedarchiver-decoder.d.ts.map +1 -0
- package/build/src/services/ios/dvt/nskeyedarchiver-decoder.js +195 -0
- package/build/src/services/ios/dvt/utils.d.ts +19 -0
- package/build/src/services/ios/dvt/utils.d.ts.map +1 -0
- package/build/src/services/ios/dvt/utils.js +67 -0
- package/build/src/services.d.ts +2 -1
- package/build/src/services.d.ts.map +1 -1
- package/build/src/services.js +26 -0
- package/package.json +5 -1
- package/src/index.ts +7 -0
- package/src/lib/plist/binary-plist-creator.ts +30 -0
- package/src/lib/plist/index.ts +2 -0
- package/src/lib/plist/plist-uid.ts +9 -0
- package/src/lib/types.ts +192 -1
- package/src/services/ios/dvt/channel-fragmenter.ts +42 -0
- package/src/services/ios/dvt/channel.ts +58 -0
- package/src/services/ios/dvt/dtx-message.ts +162 -0
- package/src/services/ios/dvt/index.ts +727 -0
- package/src/services/ios/dvt/instruments/condition-inducer.ts +140 -0
- package/src/services/ios/dvt/instruments/location-simulation.ts +83 -0
- package/src/services/ios/dvt/instruments/screenshot.ts +47 -0
- package/src/services/ios/dvt/nskeyedarchiver-decoder.ts +225 -0
- package/src/services/ios/dvt/utils.ts +89 -0
- package/src/services.ts +36 -0
|
@@ -0,0 +1,552 @@
|
|
|
1
|
+
import net from 'node:net';
|
|
2
|
+
import { getLogger } from '../../../lib/logger.js';
|
|
3
|
+
import { PlistUID, createBinaryPlist, parseBinaryPlist, } from '../../../lib/plist/index.js';
|
|
4
|
+
import { ServiceConnection } from '../../../service-connection.js';
|
|
5
|
+
import { BaseService } from '../base-service.js';
|
|
6
|
+
import { ChannelFragmenter } from './channel-fragmenter.js';
|
|
7
|
+
import { Channel } from './channel.js';
|
|
8
|
+
import { DTXMessage, DTX_CONSTANTS, MessageAux } from './dtx-message.js';
|
|
9
|
+
import { decodeNSKeyedArchiver } from './nskeyedarchiver-decoder.js';
|
|
10
|
+
import { extractCapabilityStrings, extractNSDictionary, extractNSKeyedArchiverObjects, hasNSErrorIndicators, isNSDictionaryFormat, } from './utils.js';
|
|
11
|
+
const log = getLogger('DVTSecureSocketProxyService');
|
|
12
|
+
const MIN_ERROR_DESCRIPTION_LENGTH = 20;
|
|
13
|
+
/**
|
|
14
|
+
* DVTSecureSocketProxyService provides access to Apple's DTServiceHub functionality
|
|
15
|
+
* This service enables various instruments and debugging capabilities through the DTX protocol
|
|
16
|
+
*/
|
|
17
|
+
export class DVTSecureSocketProxyService extends BaseService {
|
|
18
|
+
static RSD_SERVICE_NAME = 'com.apple.instruments.dtservicehub';
|
|
19
|
+
static BROADCAST_CHANNEL = 0;
|
|
20
|
+
connection = null;
|
|
21
|
+
socket = null;
|
|
22
|
+
supportedIdentifiers = {};
|
|
23
|
+
lastChannelCode = 0;
|
|
24
|
+
curMessageId = 0;
|
|
25
|
+
channelCache = new Map();
|
|
26
|
+
channelMessages = new Map();
|
|
27
|
+
isHandshakeComplete = false;
|
|
28
|
+
readBuffer = Buffer.alloc(0);
|
|
29
|
+
constructor(address) {
|
|
30
|
+
super(address);
|
|
31
|
+
this.channelMessages.set(DVTSecureSocketProxyService.BROADCAST_CHANNEL, new ChannelFragmenter());
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Connect to the DVT service and perform handshake
|
|
35
|
+
*/
|
|
36
|
+
async connect() {
|
|
37
|
+
if (this.connection) {
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
const service = {
|
|
41
|
+
serviceName: DVTSecureSocketProxyService.RSD_SERVICE_NAME,
|
|
42
|
+
port: this.address[1].toString(),
|
|
43
|
+
};
|
|
44
|
+
// DVT uses DTX binary protocol, connect without plist-based RSDCheckin
|
|
45
|
+
this.connection = await this.startLockdownWithoutCheckin(service);
|
|
46
|
+
this.socket = this.connection.getSocket();
|
|
47
|
+
// Remove SSL context if present for raw DTX communication
|
|
48
|
+
if ('_sslobj' in this.socket) {
|
|
49
|
+
this.socket._sslobj = null;
|
|
50
|
+
}
|
|
51
|
+
await this.performHandshake();
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Get supported service identifiers (capabilities)
|
|
55
|
+
*/
|
|
56
|
+
getSupportedIdentifiers() {
|
|
57
|
+
return this.supportedIdentifiers;
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Create a communication channel for a specific service identifier
|
|
61
|
+
* @param identifier The service identifier (e.g., 'com.apple.instruments.server.services.LocationSimulation')
|
|
62
|
+
* @returns The created channel instance
|
|
63
|
+
*/
|
|
64
|
+
async makeChannel(identifier) {
|
|
65
|
+
if (!this.isHandshakeComplete) {
|
|
66
|
+
throw new Error('Handshake not complete. Call connect() first.');
|
|
67
|
+
}
|
|
68
|
+
if (this.channelCache.has(identifier)) {
|
|
69
|
+
return this.channelCache.get(identifier);
|
|
70
|
+
}
|
|
71
|
+
this.lastChannelCode++;
|
|
72
|
+
const channelCode = this.lastChannelCode;
|
|
73
|
+
const args = new MessageAux();
|
|
74
|
+
args.appendInt(channelCode);
|
|
75
|
+
args.appendObj(identifier);
|
|
76
|
+
await this.sendMessage(0, '_requestChannelWithCode:identifier:', args);
|
|
77
|
+
const [ret] = await this.recvPlist();
|
|
78
|
+
// Check for NSError in response
|
|
79
|
+
this.checkForNSError(ret, 'Failed to create channel');
|
|
80
|
+
const channel = new Channel(channelCode, this);
|
|
81
|
+
this.channelCache.set(identifier, channel);
|
|
82
|
+
this.channelMessages.set(channelCode, new ChannelFragmenter());
|
|
83
|
+
return channel;
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Send a DTX message on a channel
|
|
87
|
+
* @param channel The channel code
|
|
88
|
+
* @param selector The ObjectiveC method selector
|
|
89
|
+
* @param args Optional message arguments
|
|
90
|
+
* @param expectsReply Whether a reply is expected
|
|
91
|
+
*/
|
|
92
|
+
async sendMessage(channel, selector = null, args = null, expectsReply = true) {
|
|
93
|
+
if (!this.socket) {
|
|
94
|
+
throw new Error('Not connected to DVT service');
|
|
95
|
+
}
|
|
96
|
+
this.curMessageId++;
|
|
97
|
+
const auxBuffer = args ? this.buildAuxiliaryData(args) : Buffer.alloc(0);
|
|
98
|
+
const selectorBuffer = selector
|
|
99
|
+
? this.archiveSelector(selector)
|
|
100
|
+
: Buffer.alloc(0);
|
|
101
|
+
let flags = DTX_CONSTANTS.INSTRUMENTS_MESSAGE_TYPE;
|
|
102
|
+
if (expectsReply) {
|
|
103
|
+
flags |= DTX_CONSTANTS.EXPECTS_REPLY_MASK;
|
|
104
|
+
}
|
|
105
|
+
const payloadHeader = DTXMessage.buildPayloadHeader({
|
|
106
|
+
flags,
|
|
107
|
+
auxiliaryLength: auxBuffer.length,
|
|
108
|
+
totalLength: BigInt(auxBuffer.length + selectorBuffer.length),
|
|
109
|
+
});
|
|
110
|
+
const messageHeader = DTXMessage.buildMessageHeader({
|
|
111
|
+
magic: DTX_CONSTANTS.MESSAGE_HEADER_MAGIC,
|
|
112
|
+
cb: DTX_CONSTANTS.MESSAGE_HEADER_SIZE,
|
|
113
|
+
fragmentId: 0,
|
|
114
|
+
fragmentCount: 1,
|
|
115
|
+
length: DTX_CONSTANTS.PAYLOAD_HEADER_SIZE +
|
|
116
|
+
auxBuffer.length +
|
|
117
|
+
selectorBuffer.length,
|
|
118
|
+
identifier: this.curMessageId,
|
|
119
|
+
conversationIndex: 0,
|
|
120
|
+
channelCode: channel,
|
|
121
|
+
expectsReply: expectsReply ? 1 : 0,
|
|
122
|
+
});
|
|
123
|
+
const message = Buffer.concat([
|
|
124
|
+
messageHeader,
|
|
125
|
+
payloadHeader,
|
|
126
|
+
auxBuffer,
|
|
127
|
+
selectorBuffer,
|
|
128
|
+
]);
|
|
129
|
+
await new Promise((resolve, reject) => {
|
|
130
|
+
this.socket.write(message, (err) => {
|
|
131
|
+
if (err) {
|
|
132
|
+
reject(err);
|
|
133
|
+
}
|
|
134
|
+
else {
|
|
135
|
+
resolve();
|
|
136
|
+
}
|
|
137
|
+
});
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
/**
|
|
141
|
+
* Receive a plist message from a channel
|
|
142
|
+
* @param channel The channel to receive from
|
|
143
|
+
* @returns Tuple of [decoded data, auxiliary values]
|
|
144
|
+
*/
|
|
145
|
+
async recvPlist(channel = DVTSecureSocketProxyService.BROADCAST_CHANNEL) {
|
|
146
|
+
const [data, aux] = await this.recvMessage(channel);
|
|
147
|
+
let decodedData = null;
|
|
148
|
+
if (data?.length) {
|
|
149
|
+
try {
|
|
150
|
+
decodedData = parseBinaryPlist(data);
|
|
151
|
+
// decode NSKeyedArchiver format
|
|
152
|
+
decodedData = decodeNSKeyedArchiver(decodedData);
|
|
153
|
+
}
|
|
154
|
+
catch (error) {
|
|
155
|
+
log.warn('Failed to parse plist data:', error);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
return [decodedData, aux];
|
|
159
|
+
}
|
|
160
|
+
/**
|
|
161
|
+
* Receive a raw message from a channel
|
|
162
|
+
* @param channel The channel to receive from
|
|
163
|
+
* @returns Tuple of [raw data, auxiliary values]
|
|
164
|
+
*/
|
|
165
|
+
async recvMessage(channel = DVTSecureSocketProxyService.BROADCAST_CHANNEL) {
|
|
166
|
+
const packetData = await this.recvPacketFragments(channel);
|
|
167
|
+
const payloadHeader = DTXMessage.parsePayloadHeader(packetData);
|
|
168
|
+
const compression = (payloadHeader.flags & 0xff000) >> 12;
|
|
169
|
+
if (compression) {
|
|
170
|
+
throw new Error('Compressed messages not supported');
|
|
171
|
+
}
|
|
172
|
+
let offset = DTX_CONSTANTS.PAYLOAD_HEADER_SIZE;
|
|
173
|
+
// Parse auxiliary data if present
|
|
174
|
+
let aux = [];
|
|
175
|
+
if (payloadHeader.auxiliaryLength > 0) {
|
|
176
|
+
const auxBuffer = packetData.subarray(offset, offset + payloadHeader.auxiliaryLength);
|
|
177
|
+
aux = this.parseAuxiliaryData(auxBuffer);
|
|
178
|
+
offset += payloadHeader.auxiliaryLength;
|
|
179
|
+
}
|
|
180
|
+
// Extract object data
|
|
181
|
+
const objSize = Number(payloadHeader.totalLength) - payloadHeader.auxiliaryLength;
|
|
182
|
+
const data = objSize > 0 ? packetData.subarray(offset, offset + objSize) : null;
|
|
183
|
+
return [data, aux];
|
|
184
|
+
}
|
|
185
|
+
/**
|
|
186
|
+
* Close the DVT service connection
|
|
187
|
+
*/
|
|
188
|
+
async close() {
|
|
189
|
+
if (!this.connection) {
|
|
190
|
+
return;
|
|
191
|
+
}
|
|
192
|
+
// Send channel cancellation for all active channels
|
|
193
|
+
const activeCodes = Array.from(this.channelMessages.keys()).filter((code) => code > 0);
|
|
194
|
+
if (activeCodes.length > 0) {
|
|
195
|
+
const args = new MessageAux();
|
|
196
|
+
for (const code of activeCodes) {
|
|
197
|
+
args.appendInt(code);
|
|
198
|
+
}
|
|
199
|
+
try {
|
|
200
|
+
await this.sendMessage(DVTSecureSocketProxyService.BROADCAST_CHANNEL, '_channelCanceled:', args, false);
|
|
201
|
+
}
|
|
202
|
+
catch (error) {
|
|
203
|
+
log.debug('Error sending channel canceled message:', error);
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
this.connection.close();
|
|
207
|
+
this.connection = null;
|
|
208
|
+
this.socket = null;
|
|
209
|
+
this.isHandshakeComplete = false;
|
|
210
|
+
this.channelCache.clear();
|
|
211
|
+
this.channelMessages.clear();
|
|
212
|
+
this.channelMessages.set(DVTSecureSocketProxyService.BROADCAST_CHANNEL, new ChannelFragmenter());
|
|
213
|
+
}
|
|
214
|
+
/**
|
|
215
|
+
* Perform DTX protocol handshake to establish connection and retrieve capabilities
|
|
216
|
+
*/
|
|
217
|
+
async performHandshake() {
|
|
218
|
+
const args = new MessageAux();
|
|
219
|
+
args.appendObj({
|
|
220
|
+
'com.apple.private.DTXBlockCompression': 0,
|
|
221
|
+
'com.apple.private.DTXConnection': 1,
|
|
222
|
+
});
|
|
223
|
+
await this.sendMessage(0, '_notifyOfPublishedCapabilities:', args, false);
|
|
224
|
+
const [retData, aux] = await this.recvMessage();
|
|
225
|
+
const ret = retData ? parseBinaryPlist(retData) : null;
|
|
226
|
+
const selectorName = this.extractSelectorFromResponse(ret);
|
|
227
|
+
if (selectorName !== '_notifyOfPublishedCapabilities:') {
|
|
228
|
+
throw new Error(`Invalid handshake response selector: ${selectorName}`);
|
|
229
|
+
}
|
|
230
|
+
if (!aux || aux.length === 0) {
|
|
231
|
+
throw new Error('Invalid handshake response: missing capabilities');
|
|
232
|
+
}
|
|
233
|
+
// Extract server capabilities from auxiliary data
|
|
234
|
+
this.supportedIdentifiers = this.extractCapabilitiesFromAuxData(aux[0]);
|
|
235
|
+
this.isHandshakeComplete = true;
|
|
236
|
+
log.debug(`DVT handshake complete. Found ${Object.keys(this.supportedIdentifiers).length} supported identifiers`);
|
|
237
|
+
// Consume any additional messages buffered after handshake
|
|
238
|
+
await this.drainBufferedMessages();
|
|
239
|
+
}
|
|
240
|
+
extractSelectorFromResponse(ret) {
|
|
241
|
+
if (typeof ret === 'string') {
|
|
242
|
+
return ret;
|
|
243
|
+
}
|
|
244
|
+
const objects = extractNSKeyedArchiverObjects(ret);
|
|
245
|
+
if (objects) {
|
|
246
|
+
return objects[1];
|
|
247
|
+
}
|
|
248
|
+
throw new Error('Invalid handshake response');
|
|
249
|
+
}
|
|
250
|
+
extractCapabilitiesFromAuxData(capabilitiesData) {
|
|
251
|
+
const objects = extractNSKeyedArchiverObjects(capabilitiesData);
|
|
252
|
+
if (!objects) {
|
|
253
|
+
return capabilitiesData || {};
|
|
254
|
+
}
|
|
255
|
+
const dictObj = objects[1];
|
|
256
|
+
if (isNSDictionaryFormat(dictObj)) {
|
|
257
|
+
return extractNSDictionary(dictObj, objects);
|
|
258
|
+
}
|
|
259
|
+
return extractCapabilityStrings(objects);
|
|
260
|
+
}
|
|
261
|
+
/**
|
|
262
|
+
* Drain any buffered messages that arrived during handshake
|
|
263
|
+
*/
|
|
264
|
+
async drainBufferedMessages() {
|
|
265
|
+
if (this.readBuffer.length === 0) {
|
|
266
|
+
return;
|
|
267
|
+
}
|
|
268
|
+
try {
|
|
269
|
+
while (this.readBuffer.length >= DTX_CONSTANTS.MESSAGE_HEADER_SIZE) {
|
|
270
|
+
const headerData = this.readBuffer.subarray(0, DTX_CONSTANTS.MESSAGE_HEADER_SIZE);
|
|
271
|
+
const header = DTXMessage.parseMessageHeader(headerData);
|
|
272
|
+
const totalSize = DTX_CONSTANTS.MESSAGE_HEADER_SIZE + header.length;
|
|
273
|
+
if (this.readBuffer.length >= totalSize) {
|
|
274
|
+
// Consume complete buffered message
|
|
275
|
+
this.readBuffer = this.readBuffer.subarray(DTX_CONSTANTS.MESSAGE_HEADER_SIZE);
|
|
276
|
+
this.readBuffer = this.readBuffer.subarray(header.length);
|
|
277
|
+
}
|
|
278
|
+
else {
|
|
279
|
+
break;
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
catch (error) {
|
|
284
|
+
log.debug('Error while draining buffer:', error);
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
/**
|
|
288
|
+
* Receive packet fragments until a complete message is available for the specified channel
|
|
289
|
+
*/
|
|
290
|
+
async recvPacketFragments(channel) {
|
|
291
|
+
while (true) {
|
|
292
|
+
const fragmenter = this.channelMessages.get(channel);
|
|
293
|
+
if (!fragmenter) {
|
|
294
|
+
throw new Error(`No fragmenter for channel ${channel}`);
|
|
295
|
+
}
|
|
296
|
+
// Check if we have a complete message
|
|
297
|
+
const message = fragmenter.get();
|
|
298
|
+
if (message) {
|
|
299
|
+
return message;
|
|
300
|
+
}
|
|
301
|
+
// Read next message header
|
|
302
|
+
const headerData = await this.readExact(DTX_CONSTANTS.MESSAGE_HEADER_SIZE);
|
|
303
|
+
const header = DTXMessage.parseMessageHeader(headerData);
|
|
304
|
+
const receivedChannel = Math.abs(header.channelCode);
|
|
305
|
+
if (!this.channelMessages.has(receivedChannel)) {
|
|
306
|
+
this.channelMessages.set(receivedChannel, new ChannelFragmenter());
|
|
307
|
+
}
|
|
308
|
+
// Update message ID tracker
|
|
309
|
+
if (!header.conversationIndex && header.identifier > this.curMessageId) {
|
|
310
|
+
this.curMessageId = header.identifier;
|
|
311
|
+
}
|
|
312
|
+
// Skip first fragment header for multi-fragment messages
|
|
313
|
+
if (header.fragmentCount > 1 && header.fragmentId === 0) {
|
|
314
|
+
continue;
|
|
315
|
+
}
|
|
316
|
+
// Read message payload
|
|
317
|
+
const messageData = await this.readExact(header.length);
|
|
318
|
+
// Add fragment to appropriate channel
|
|
319
|
+
const targetFragmenter = this.channelMessages.get(receivedChannel);
|
|
320
|
+
targetFragmenter.addFragment(header, messageData);
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
/**
|
|
324
|
+
* Read exact number of bytes from socket with buffering
|
|
325
|
+
*/
|
|
326
|
+
async readExact(length) {
|
|
327
|
+
if (!this.socket) {
|
|
328
|
+
throw new Error(`${this.constructor.name} is not initialized. Call connect() before sending messages.`);
|
|
329
|
+
}
|
|
330
|
+
// Keep reading until we have enough data
|
|
331
|
+
while (this.readBuffer.length < length) {
|
|
332
|
+
const chunk = await new Promise((resolve, reject) => {
|
|
333
|
+
const onData = (data) => {
|
|
334
|
+
this.socket.off('data', onData);
|
|
335
|
+
this.socket.off('error', onError);
|
|
336
|
+
resolve(data);
|
|
337
|
+
};
|
|
338
|
+
const onError = (err) => {
|
|
339
|
+
this.socket.off('data', onData);
|
|
340
|
+
this.socket.off('error', onError);
|
|
341
|
+
reject(err);
|
|
342
|
+
};
|
|
343
|
+
this.socket.once('data', onData);
|
|
344
|
+
this.socket.once('error', onError);
|
|
345
|
+
});
|
|
346
|
+
this.readBuffer = Buffer.concat([this.readBuffer, chunk]);
|
|
347
|
+
}
|
|
348
|
+
// Extract exact amount requested
|
|
349
|
+
const result = this.readBuffer.subarray(0, length);
|
|
350
|
+
this.readBuffer = this.readBuffer.subarray(length);
|
|
351
|
+
return result;
|
|
352
|
+
}
|
|
353
|
+
/**
|
|
354
|
+
* Check if response contains an NSError and throw if present
|
|
355
|
+
*/
|
|
356
|
+
checkForNSError(response, context) {
|
|
357
|
+
if (!response || typeof response !== 'object') {
|
|
358
|
+
return;
|
|
359
|
+
}
|
|
360
|
+
// Check NSKeyedArchiver format
|
|
361
|
+
const objects = extractNSKeyedArchiverObjects(response);
|
|
362
|
+
if (objects) {
|
|
363
|
+
// Check for NSError indicators in $objects
|
|
364
|
+
const hasNSError = objects.some((o) => hasNSErrorIndicators(o));
|
|
365
|
+
if (hasNSError) {
|
|
366
|
+
const errorMsg = objects.find((o) => typeof o === 'string' && o.length > MIN_ERROR_DESCRIPTION_LENGTH) || 'Unknown error';
|
|
367
|
+
throw new Error(`${context}: ${errorMsg}`);
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
// Check direct NSError format
|
|
371
|
+
if (hasNSErrorIndicators(response)) {
|
|
372
|
+
throw new Error(`${context}: ${JSON.stringify(response)}`);
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
/**
|
|
376
|
+
* Archive a value using NSKeyedArchiver format for DTX protocol
|
|
377
|
+
*/
|
|
378
|
+
archiveValue(value) {
|
|
379
|
+
const archived = {
|
|
380
|
+
$version: 100000,
|
|
381
|
+
$archiver: 'NSKeyedArchiver',
|
|
382
|
+
$top: { root: new PlistUID(1) },
|
|
383
|
+
$objects: ['$null', value],
|
|
384
|
+
};
|
|
385
|
+
return createBinaryPlist(archived);
|
|
386
|
+
}
|
|
387
|
+
/**
|
|
388
|
+
* Archive a selector string for DTX messages
|
|
389
|
+
*/
|
|
390
|
+
archiveSelector(selector) {
|
|
391
|
+
return this.archiveValue(selector);
|
|
392
|
+
}
|
|
393
|
+
/**
|
|
394
|
+
* Build auxiliary data buffer with NSKeyedArchiver encoding for objects
|
|
395
|
+
*/
|
|
396
|
+
buildAuxiliaryData(args) {
|
|
397
|
+
const values = args.getValues();
|
|
398
|
+
if (values.length === 0) {
|
|
399
|
+
return Buffer.alloc(0);
|
|
400
|
+
}
|
|
401
|
+
const itemBuffers = [];
|
|
402
|
+
for (const auxValue of values) {
|
|
403
|
+
// Empty dictionary marker
|
|
404
|
+
const dictMarker = Buffer.alloc(4);
|
|
405
|
+
dictMarker.writeUInt32LE(DTX_CONSTANTS.EMPTY_DICTIONARY, 0);
|
|
406
|
+
itemBuffers.push(dictMarker);
|
|
407
|
+
// Type marker
|
|
408
|
+
const typeBuffer = Buffer.alloc(4);
|
|
409
|
+
typeBuffer.writeUInt32LE(auxValue.type, 0);
|
|
410
|
+
itemBuffers.push(typeBuffer);
|
|
411
|
+
// Value data
|
|
412
|
+
switch (auxValue.type) {
|
|
413
|
+
case DTX_CONSTANTS.AUX_TYPE_INT32: {
|
|
414
|
+
const valueBuffer = Buffer.alloc(4);
|
|
415
|
+
valueBuffer.writeUInt32LE(auxValue.value, 0);
|
|
416
|
+
itemBuffers.push(valueBuffer);
|
|
417
|
+
break;
|
|
418
|
+
}
|
|
419
|
+
case DTX_CONSTANTS.AUX_TYPE_INT64: {
|
|
420
|
+
const valueBuffer = Buffer.alloc(8);
|
|
421
|
+
valueBuffer.writeBigUInt64LE(BigInt(auxValue.value), 0);
|
|
422
|
+
itemBuffers.push(valueBuffer);
|
|
423
|
+
break;
|
|
424
|
+
}
|
|
425
|
+
case DTX_CONSTANTS.AUX_TYPE_OBJECT: {
|
|
426
|
+
const encodedPlist = this.archiveValue(auxValue.value);
|
|
427
|
+
const lengthBuffer = Buffer.alloc(4);
|
|
428
|
+
lengthBuffer.writeUInt32LE(encodedPlist.length, 0);
|
|
429
|
+
itemBuffers.push(lengthBuffer);
|
|
430
|
+
itemBuffers.push(encodedPlist);
|
|
431
|
+
break;
|
|
432
|
+
}
|
|
433
|
+
default:
|
|
434
|
+
throw new Error(`Unsupported auxiliary type: ${auxValue.type}`);
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
const itemsData = Buffer.concat(itemBuffers);
|
|
438
|
+
// Build header: magic + total size of items
|
|
439
|
+
const header = Buffer.alloc(16);
|
|
440
|
+
header.writeBigUInt64LE(BigInt(DTX_CONSTANTS.MESSAGE_AUX_MAGIC), 0);
|
|
441
|
+
header.writeBigUInt64LE(BigInt(itemsData.length), 8);
|
|
442
|
+
return Buffer.concat([header, itemsData]);
|
|
443
|
+
}
|
|
444
|
+
/**
|
|
445
|
+
* Parse auxiliary data from buffer
|
|
446
|
+
*
|
|
447
|
+
* The auxiliary data format can be:
|
|
448
|
+
* 1. Standard format: [magic:8][size:8][items...]
|
|
449
|
+
* 2. NSKeyedArchiver bplist format (for handshake responses)
|
|
450
|
+
*/
|
|
451
|
+
parseAuxiliaryData(buffer) {
|
|
452
|
+
if (buffer.length < 16) {
|
|
453
|
+
return [];
|
|
454
|
+
}
|
|
455
|
+
const magic = buffer.readBigUInt64LE(0);
|
|
456
|
+
// Check if this is NSKeyedArchiver bplist format (handshake response)
|
|
457
|
+
if (magic !== BigInt(DTX_CONSTANTS.MESSAGE_AUX_MAGIC)) {
|
|
458
|
+
return this.parseAuxiliaryAsBplist(buffer);
|
|
459
|
+
}
|
|
460
|
+
// Standard auxiliary format
|
|
461
|
+
return this.parseAuxiliaryStandard(buffer);
|
|
462
|
+
}
|
|
463
|
+
/**
|
|
464
|
+
* Parse auxiliary data in NSKeyedArchiver bplist format
|
|
465
|
+
*/
|
|
466
|
+
parseAuxiliaryAsBplist(buffer) {
|
|
467
|
+
// Find bplist header in buffer
|
|
468
|
+
const bplistMagic = 'bplist00';
|
|
469
|
+
for (let i = 0; i < Math.min(100, buffer.length - 8); i++) {
|
|
470
|
+
if (buffer.toString('ascii', i, i + 8) === bplistMagic) {
|
|
471
|
+
try {
|
|
472
|
+
const plistBuffer = buffer.subarray(i);
|
|
473
|
+
const parsed = parseBinaryPlist(plistBuffer);
|
|
474
|
+
return Array.isArray(parsed) ? parsed : [parsed];
|
|
475
|
+
}
|
|
476
|
+
catch (error) {
|
|
477
|
+
log.warn('Failed to parse auxiliary bplist:', error);
|
|
478
|
+
}
|
|
479
|
+
break;
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
return [];
|
|
483
|
+
}
|
|
484
|
+
/**
|
|
485
|
+
* Parse auxiliary data in standard DTX format
|
|
486
|
+
*/
|
|
487
|
+
parseAuxiliaryStandard(buffer) {
|
|
488
|
+
const values = [];
|
|
489
|
+
let offset = 16; // Skip magic (8) + size (8)
|
|
490
|
+
const totalSize = buffer.readBigUInt64LE(8);
|
|
491
|
+
const endOffset = offset + Number(totalSize);
|
|
492
|
+
while (offset < endOffset && offset < buffer.length) {
|
|
493
|
+
// Read and validate empty dictionary marker
|
|
494
|
+
const marker = buffer.readUInt32LE(offset);
|
|
495
|
+
offset += 4;
|
|
496
|
+
if (marker !== DTX_CONSTANTS.EMPTY_DICTIONARY) {
|
|
497
|
+
offset -= 4; // Rewind if not the expected marker
|
|
498
|
+
}
|
|
499
|
+
// Read type
|
|
500
|
+
const type = buffer.readUInt32LE(offset);
|
|
501
|
+
offset += 4;
|
|
502
|
+
// Read value based on type
|
|
503
|
+
try {
|
|
504
|
+
const value = this.parseAuxiliaryValue(buffer, type, offset);
|
|
505
|
+
values.push(value.data);
|
|
506
|
+
offset = value.newOffset;
|
|
507
|
+
}
|
|
508
|
+
catch (error) {
|
|
509
|
+
log.warn(`Failed to parse auxiliary value at offset ${offset}:`, error);
|
|
510
|
+
break;
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
return values;
|
|
514
|
+
}
|
|
515
|
+
/**
|
|
516
|
+
* Parse a single auxiliary value
|
|
517
|
+
*/
|
|
518
|
+
parseAuxiliaryValue(buffer, type, offset) {
|
|
519
|
+
switch (type) {
|
|
520
|
+
case DTX_CONSTANTS.AUX_TYPE_INT32:
|
|
521
|
+
return {
|
|
522
|
+
data: buffer.readUInt32LE(offset),
|
|
523
|
+
newOffset: offset + 4,
|
|
524
|
+
};
|
|
525
|
+
case DTX_CONSTANTS.AUX_TYPE_INT64:
|
|
526
|
+
return {
|
|
527
|
+
data: buffer.readBigUInt64LE(offset),
|
|
528
|
+
newOffset: offset + 8,
|
|
529
|
+
};
|
|
530
|
+
case DTX_CONSTANTS.AUX_TYPE_OBJECT: {
|
|
531
|
+
const length = buffer.readUInt32LE(offset);
|
|
532
|
+
const plistData = buffer.subarray(offset + 4, offset + 4 + length);
|
|
533
|
+
let parsed;
|
|
534
|
+
try {
|
|
535
|
+
parsed = parseBinaryPlist(plistData);
|
|
536
|
+
}
|
|
537
|
+
catch (error) {
|
|
538
|
+
log.warn('Failed to parse auxiliary object plist:', error);
|
|
539
|
+
parsed = plistData;
|
|
540
|
+
}
|
|
541
|
+
return {
|
|
542
|
+
data: parsed,
|
|
543
|
+
newOffset: offset + 4 + length,
|
|
544
|
+
};
|
|
545
|
+
}
|
|
546
|
+
default:
|
|
547
|
+
throw new Error(`Unknown auxiliary type: ${type}`);
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
export { Channel, ChannelFragmenter, DTXMessage, MessageAux, DTX_CONSTANTS };
|
|
552
|
+
export { decodeNSKeyedArchiver, NSKeyedArchiverDecoder, } from './nskeyedarchiver-decoder.js';
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import type { ConditionGroup } from '../../../../lib/types.js';
|
|
2
|
+
import type { DVTSecureSocketProxyService } from '../index.js';
|
|
3
|
+
/**
|
|
4
|
+
* Condition Inducer service for simulating various device conditions
|
|
5
|
+
* such as network conditions, thermal states, etc.
|
|
6
|
+
*/
|
|
7
|
+
export declare class ConditionInducer {
|
|
8
|
+
private readonly dvt;
|
|
9
|
+
static readonly IDENTIFIER = "com.apple.instruments.server.services.ConditionInducer";
|
|
10
|
+
private channel;
|
|
11
|
+
constructor(dvt: DVTSecureSocketProxyService);
|
|
12
|
+
/**
|
|
13
|
+
* Initialize the condition inducer channel
|
|
14
|
+
*/
|
|
15
|
+
initialize(): Promise<void>;
|
|
16
|
+
/**
|
|
17
|
+
* List all available condition inducers and their profiles
|
|
18
|
+
* @returns Array of condition groups with their available profiles
|
|
19
|
+
*/
|
|
20
|
+
list(): Promise<ConditionGroup[]>;
|
|
21
|
+
/**
|
|
22
|
+
* Set a specific condition profile
|
|
23
|
+
* @param profileIdentifier The identifier of the profile to enable
|
|
24
|
+
* @throws Error if the profile identifier is not found
|
|
25
|
+
* @throws Error if a condition is already active
|
|
26
|
+
*/
|
|
27
|
+
set(profileIdentifier: string): Promise<void>;
|
|
28
|
+
/**
|
|
29
|
+
* Disable the currently active condition
|
|
30
|
+
*
|
|
31
|
+
* Note: This method is idempotent - calling it when no condition is active
|
|
32
|
+
* will not throw an error.
|
|
33
|
+
*/
|
|
34
|
+
disable(): Promise<void>;
|
|
35
|
+
private isNSError;
|
|
36
|
+
}
|
|
37
|
+
//# sourceMappingURL=condition-inducer.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"condition-inducer.d.ts","sourceRoot":"","sources":["../../../../../../src/services/ios/dvt/instruments/condition-inducer.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,0BAA0B,CAAC;AAG/D,OAAO,KAAK,EAAE,2BAA2B,EAAE,MAAM,aAAa,CAAC;AAI/D;;;GAGG;AACH,qBAAa,gBAAgB;IAMf,OAAO,CAAC,QAAQ,CAAC,GAAG;IALhC,MAAM,CAAC,QAAQ,CAAC,UAAU,4DACiC;IAE3D,OAAO,CAAC,OAAO,CAAwB;gBAEV,GAAG,EAAE,2BAA2B;IAE7D;;OAEG;IACG,UAAU,IAAI,OAAO,CAAC,IAAI,CAAC;IAOjC;;;OAGG;IACG,IAAI,IAAI,OAAO,CAAC,cAAc,EAAE,CAAC;IAwBvC;;;;;OAKG;IACG,GAAG,CAAC,iBAAiB,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IA4CnD;;;;;OAKG;IACG,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC;IAoB9B,OAAO,CAAC,SAAS;CAKlB"}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { getLogger } from '../../../../lib/logger.js';
|
|
2
|
+
import { MessageAux } from '../dtx-message.js';
|
|
3
|
+
const log = getLogger('ConditionInducer');
|
|
4
|
+
/**
|
|
5
|
+
* Condition Inducer service for simulating various device conditions
|
|
6
|
+
* such as network conditions, thermal states, etc.
|
|
7
|
+
*/
|
|
8
|
+
export class ConditionInducer {
|
|
9
|
+
dvt;
|
|
10
|
+
static IDENTIFIER = 'com.apple.instruments.server.services.ConditionInducer';
|
|
11
|
+
channel = null;
|
|
12
|
+
constructor(dvt) {
|
|
13
|
+
this.dvt = dvt;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Initialize the condition inducer channel
|
|
17
|
+
*/
|
|
18
|
+
async initialize() {
|
|
19
|
+
if (this.channel) {
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
this.channel = await this.dvt.makeChannel(ConditionInducer.IDENTIFIER);
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* List all available condition inducers and their profiles
|
|
26
|
+
* @returns Array of condition groups with their available profiles
|
|
27
|
+
*/
|
|
28
|
+
async list() {
|
|
29
|
+
await this.initialize();
|
|
30
|
+
await this.channel.call('availableConditionInducers')();
|
|
31
|
+
const result = await this.channel.receivePlist();
|
|
32
|
+
// Handle different response formats
|
|
33
|
+
if (!result) {
|
|
34
|
+
log.warn('Received null/undefined response from availableConditionInducers');
|
|
35
|
+
return [];
|
|
36
|
+
}
|
|
37
|
+
// If result is already an array, return it
|
|
38
|
+
if (Array.isArray(result)) {
|
|
39
|
+
return result;
|
|
40
|
+
}
|
|
41
|
+
throw new Error(`Unexpected response format from availableConditionInducers: ${JSON.stringify(result)}`);
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Set a specific condition profile
|
|
45
|
+
* @param profileIdentifier The identifier of the profile to enable
|
|
46
|
+
* @throws Error if the profile identifier is not found
|
|
47
|
+
* @throws Error if a condition is already active
|
|
48
|
+
*/
|
|
49
|
+
async set(profileIdentifier) {
|
|
50
|
+
await this.initialize();
|
|
51
|
+
const groups = await this.list();
|
|
52
|
+
// Find the profile in the available groups
|
|
53
|
+
for (const group of groups) {
|
|
54
|
+
const profiles = group.profiles || [];
|
|
55
|
+
for (const profile of profiles) {
|
|
56
|
+
if (profileIdentifier !== profile.identifier) {
|
|
57
|
+
continue;
|
|
58
|
+
}
|
|
59
|
+
log.info(`Enabling condition: ${profile.description || profile.identifier}`);
|
|
60
|
+
const args = new MessageAux()
|
|
61
|
+
.appendObj(group.identifier)
|
|
62
|
+
.appendObj(profile.identifier);
|
|
63
|
+
await this.channel.call('enableConditionWithIdentifier_profileIdentifier_')(args);
|
|
64
|
+
// Wait for response which may be a raised NSError
|
|
65
|
+
await this.channel.receivePlist();
|
|
66
|
+
log.info(`Successfully enabled condition profile: ${profileIdentifier}`);
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
const availableProfiles = groups.flatMap((group) => (group.profiles || []).map((p) => p.identifier));
|
|
71
|
+
throw new Error(`Invalid profile identifier: ${profileIdentifier}. Available profiles: ${availableProfiles.join(', ')}`);
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Disable the currently active condition
|
|
75
|
+
*
|
|
76
|
+
* Note: This method is idempotent - calling it when no condition is active
|
|
77
|
+
* will not throw an error.
|
|
78
|
+
*/
|
|
79
|
+
async disable() {
|
|
80
|
+
await this.initialize();
|
|
81
|
+
await this.channel.call('disableActiveCondition')();
|
|
82
|
+
const response = await this.channel.receivePlist();
|
|
83
|
+
// Response can be:
|
|
84
|
+
// - true (successfully disabled condition)
|
|
85
|
+
// - NSError object, when no condition is active
|
|
86
|
+
if (response === true) {
|
|
87
|
+
log.info('Disabled active condition');
|
|
88
|
+
}
|
|
89
|
+
else if (this.isNSError(response)) {
|
|
90
|
+
log.debug('No active condition to disable');
|
|
91
|
+
}
|
|
92
|
+
else {
|
|
93
|
+
throw new Error(`Unexpected response from disableActiveCondition: ${JSON.stringify(response)}`);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
isNSError(obj) {
|
|
97
|
+
return ['NSCode', 'NSUserInfo', 'NSDomain'].some((prop) => obj?.[prop] !== undefined);
|
|
98
|
+
}
|
|
99
|
+
}
|