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,727 @@
|
|
|
1
|
+
import net from 'node:net';
|
|
2
|
+
|
|
3
|
+
import { getLogger } from '../../../lib/logger.js';
|
|
4
|
+
import {
|
|
5
|
+
PlistUID,
|
|
6
|
+
createBinaryPlist,
|
|
7
|
+
parseBinaryPlist,
|
|
8
|
+
} from '../../../lib/plist/index.js';
|
|
9
|
+
import type { PlistDictionary } from '../../../lib/types.js';
|
|
10
|
+
import { ServiceConnection } from '../../../service-connection.js';
|
|
11
|
+
import { BaseService, type Service } from '../base-service.js';
|
|
12
|
+
import { ChannelFragmenter } from './channel-fragmenter.js';
|
|
13
|
+
import { Channel } from './channel.js';
|
|
14
|
+
import { DTXMessage, DTX_CONSTANTS, MessageAux } from './dtx-message.js';
|
|
15
|
+
import { decodeNSKeyedArchiver } from './nskeyedarchiver-decoder.js';
|
|
16
|
+
import {
|
|
17
|
+
extractCapabilityStrings,
|
|
18
|
+
extractNSDictionary,
|
|
19
|
+
extractNSKeyedArchiverObjects,
|
|
20
|
+
hasNSErrorIndicators,
|
|
21
|
+
isNSDictionaryFormat,
|
|
22
|
+
} from './utils.js';
|
|
23
|
+
|
|
24
|
+
const log = getLogger('DVTSecureSocketProxyService');
|
|
25
|
+
|
|
26
|
+
const MIN_ERROR_DESCRIPTION_LENGTH = 20;
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* DVTSecureSocketProxyService provides access to Apple's DTServiceHub functionality
|
|
30
|
+
* This service enables various instruments and debugging capabilities through the DTX protocol
|
|
31
|
+
*/
|
|
32
|
+
export class DVTSecureSocketProxyService extends BaseService {
|
|
33
|
+
static readonly RSD_SERVICE_NAME = 'com.apple.instruments.dtservicehub';
|
|
34
|
+
static readonly BROADCAST_CHANNEL = 0;
|
|
35
|
+
|
|
36
|
+
private connection: ServiceConnection | null = null;
|
|
37
|
+
private socket: net.Socket | null = null;
|
|
38
|
+
private supportedIdentifiers: PlistDictionary = {};
|
|
39
|
+
private lastChannelCode: number = 0;
|
|
40
|
+
private curMessageId: number = 0;
|
|
41
|
+
private readonly channelCache: Map<string, Channel> = new Map();
|
|
42
|
+
private readonly channelMessages: Map<number, ChannelFragmenter> = new Map();
|
|
43
|
+
private isHandshakeComplete: boolean = false;
|
|
44
|
+
private readBuffer: Buffer = Buffer.alloc(0);
|
|
45
|
+
|
|
46
|
+
constructor(address: [string, number]) {
|
|
47
|
+
super(address);
|
|
48
|
+
this.channelMessages.set(
|
|
49
|
+
DVTSecureSocketProxyService.BROADCAST_CHANNEL,
|
|
50
|
+
new ChannelFragmenter(),
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Connect to the DVT service and perform handshake
|
|
56
|
+
*/
|
|
57
|
+
async connect(): Promise<void> {
|
|
58
|
+
if (this.connection) {
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const service: Service = {
|
|
63
|
+
serviceName: DVTSecureSocketProxyService.RSD_SERVICE_NAME,
|
|
64
|
+
port: this.address[1].toString(),
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
// DVT uses DTX binary protocol, connect without plist-based RSDCheckin
|
|
68
|
+
this.connection = await this.startLockdownWithoutCheckin(service);
|
|
69
|
+
this.socket = this.connection.getSocket();
|
|
70
|
+
|
|
71
|
+
// Remove SSL context if present for raw DTX communication
|
|
72
|
+
if ('_sslobj' in this.socket) {
|
|
73
|
+
(this.socket as any)._sslobj = null;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
await this.performHandshake();
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Get supported service identifiers (capabilities)
|
|
81
|
+
*/
|
|
82
|
+
getSupportedIdentifiers(): PlistDictionary {
|
|
83
|
+
return this.supportedIdentifiers;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Create a communication channel for a specific service identifier
|
|
88
|
+
* @param identifier The service identifier (e.g., 'com.apple.instruments.server.services.LocationSimulation')
|
|
89
|
+
* @returns The created channel instance
|
|
90
|
+
*/
|
|
91
|
+
async makeChannel(identifier: string): Promise<Channel> {
|
|
92
|
+
if (!this.isHandshakeComplete) {
|
|
93
|
+
throw new Error('Handshake not complete. Call connect() first.');
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
if (this.channelCache.has(identifier)) {
|
|
97
|
+
return this.channelCache.get(identifier)!;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
this.lastChannelCode++;
|
|
101
|
+
const channelCode = this.lastChannelCode;
|
|
102
|
+
|
|
103
|
+
const args = new MessageAux();
|
|
104
|
+
args.appendInt(channelCode);
|
|
105
|
+
args.appendObj(identifier);
|
|
106
|
+
|
|
107
|
+
await this.sendMessage(0, '_requestChannelWithCode:identifier:', args);
|
|
108
|
+
|
|
109
|
+
const [ret] = await this.recvPlist();
|
|
110
|
+
|
|
111
|
+
// Check for NSError in response
|
|
112
|
+
this.checkForNSError(ret, 'Failed to create channel');
|
|
113
|
+
|
|
114
|
+
const channel = new Channel(channelCode, this);
|
|
115
|
+
this.channelCache.set(identifier, channel);
|
|
116
|
+
this.channelMessages.set(channelCode, new ChannelFragmenter());
|
|
117
|
+
|
|
118
|
+
return channel;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Send a DTX message on a channel
|
|
123
|
+
* @param channel The channel code
|
|
124
|
+
* @param selector The ObjectiveC method selector
|
|
125
|
+
* @param args Optional message arguments
|
|
126
|
+
* @param expectsReply Whether a reply is expected
|
|
127
|
+
*/
|
|
128
|
+
async sendMessage(
|
|
129
|
+
channel: number,
|
|
130
|
+
selector: string | null = null,
|
|
131
|
+
args: MessageAux | null = null,
|
|
132
|
+
expectsReply: boolean = true,
|
|
133
|
+
): Promise<void> {
|
|
134
|
+
if (!this.socket) {
|
|
135
|
+
throw new Error('Not connected to DVT service');
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
this.curMessageId++;
|
|
139
|
+
|
|
140
|
+
const auxBuffer = args ? this.buildAuxiliaryData(args) : Buffer.alloc(0);
|
|
141
|
+
const selectorBuffer = selector
|
|
142
|
+
? this.archiveSelector(selector)
|
|
143
|
+
: Buffer.alloc(0);
|
|
144
|
+
|
|
145
|
+
let flags = DTX_CONSTANTS.INSTRUMENTS_MESSAGE_TYPE;
|
|
146
|
+
if (expectsReply) {
|
|
147
|
+
flags |= DTX_CONSTANTS.EXPECTS_REPLY_MASK;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
const payloadHeader = DTXMessage.buildPayloadHeader({
|
|
151
|
+
flags,
|
|
152
|
+
auxiliaryLength: auxBuffer.length,
|
|
153
|
+
totalLength: BigInt(auxBuffer.length + selectorBuffer.length),
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
const messageHeader = DTXMessage.buildMessageHeader({
|
|
157
|
+
magic: DTX_CONSTANTS.MESSAGE_HEADER_MAGIC,
|
|
158
|
+
cb: DTX_CONSTANTS.MESSAGE_HEADER_SIZE,
|
|
159
|
+
fragmentId: 0,
|
|
160
|
+
fragmentCount: 1,
|
|
161
|
+
length:
|
|
162
|
+
DTX_CONSTANTS.PAYLOAD_HEADER_SIZE +
|
|
163
|
+
auxBuffer.length +
|
|
164
|
+
selectorBuffer.length,
|
|
165
|
+
identifier: this.curMessageId,
|
|
166
|
+
conversationIndex: 0,
|
|
167
|
+
channelCode: channel,
|
|
168
|
+
expectsReply: expectsReply ? 1 : 0,
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
const message = Buffer.concat([
|
|
172
|
+
messageHeader,
|
|
173
|
+
payloadHeader,
|
|
174
|
+
auxBuffer,
|
|
175
|
+
selectorBuffer,
|
|
176
|
+
]);
|
|
177
|
+
|
|
178
|
+
await new Promise<void>((resolve, reject) => {
|
|
179
|
+
this.socket!.write(message, (err) => {
|
|
180
|
+
if (err) {
|
|
181
|
+
reject(err);
|
|
182
|
+
} else {
|
|
183
|
+
resolve();
|
|
184
|
+
}
|
|
185
|
+
});
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* Receive a plist message from a channel
|
|
191
|
+
* @param channel The channel to receive from
|
|
192
|
+
* @returns Tuple of [decoded data, auxiliary values]
|
|
193
|
+
*/
|
|
194
|
+
async recvPlist(
|
|
195
|
+
channel: number = DVTSecureSocketProxyService.BROADCAST_CHANNEL,
|
|
196
|
+
): Promise<[any, any[]]> {
|
|
197
|
+
const [data, aux] = await this.recvMessage(channel);
|
|
198
|
+
|
|
199
|
+
let decodedData = null;
|
|
200
|
+
if (data?.length) {
|
|
201
|
+
try {
|
|
202
|
+
decodedData = parseBinaryPlist(data);
|
|
203
|
+
// decode NSKeyedArchiver format
|
|
204
|
+
decodedData = decodeNSKeyedArchiver(decodedData);
|
|
205
|
+
} catch (error) {
|
|
206
|
+
log.warn('Failed to parse plist data:', error);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
return [decodedData, aux];
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* Receive a raw message from a channel
|
|
215
|
+
* @param channel The channel to receive from
|
|
216
|
+
* @returns Tuple of [raw data, auxiliary values]
|
|
217
|
+
*/
|
|
218
|
+
async recvMessage(
|
|
219
|
+
channel: number = DVTSecureSocketProxyService.BROADCAST_CHANNEL,
|
|
220
|
+
): Promise<[Buffer | null, any[]]> {
|
|
221
|
+
const packetData = await this.recvPacketFragments(channel);
|
|
222
|
+
|
|
223
|
+
const payloadHeader = DTXMessage.parsePayloadHeader(packetData);
|
|
224
|
+
|
|
225
|
+
const compression = (payloadHeader.flags & 0xff000) >> 12;
|
|
226
|
+
if (compression) {
|
|
227
|
+
throw new Error('Compressed messages not supported');
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
let offset = DTX_CONSTANTS.PAYLOAD_HEADER_SIZE;
|
|
231
|
+
|
|
232
|
+
// Parse auxiliary data if present
|
|
233
|
+
let aux: any[] = [];
|
|
234
|
+
if (payloadHeader.auxiliaryLength > 0) {
|
|
235
|
+
const auxBuffer = packetData.subarray(
|
|
236
|
+
offset,
|
|
237
|
+
offset + payloadHeader.auxiliaryLength,
|
|
238
|
+
);
|
|
239
|
+
aux = this.parseAuxiliaryData(auxBuffer);
|
|
240
|
+
offset += payloadHeader.auxiliaryLength;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// Extract object data
|
|
244
|
+
const objSize =
|
|
245
|
+
Number(payloadHeader.totalLength) - payloadHeader.auxiliaryLength;
|
|
246
|
+
const data =
|
|
247
|
+
objSize > 0 ? packetData.subarray(offset, offset + objSize) : null;
|
|
248
|
+
|
|
249
|
+
return [data, aux];
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/**
|
|
253
|
+
* Close the DVT service connection
|
|
254
|
+
*/
|
|
255
|
+
async close(): Promise<void> {
|
|
256
|
+
if (!this.connection) {
|
|
257
|
+
return;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
// Send channel cancellation for all active channels
|
|
261
|
+
const activeCodes = Array.from(this.channelMessages.keys()).filter(
|
|
262
|
+
(code) => code > 0,
|
|
263
|
+
);
|
|
264
|
+
|
|
265
|
+
if (activeCodes.length > 0) {
|
|
266
|
+
const args = new MessageAux();
|
|
267
|
+
for (const code of activeCodes) {
|
|
268
|
+
args.appendInt(code);
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
try {
|
|
272
|
+
await this.sendMessage(
|
|
273
|
+
DVTSecureSocketProxyService.BROADCAST_CHANNEL,
|
|
274
|
+
'_channelCanceled:',
|
|
275
|
+
args,
|
|
276
|
+
false,
|
|
277
|
+
);
|
|
278
|
+
} catch (error) {
|
|
279
|
+
log.debug('Error sending channel canceled message:', error);
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
this.connection.close();
|
|
284
|
+
this.connection = null;
|
|
285
|
+
this.socket = null;
|
|
286
|
+
this.isHandshakeComplete = false;
|
|
287
|
+
this.channelCache.clear();
|
|
288
|
+
this.channelMessages.clear();
|
|
289
|
+
this.channelMessages.set(
|
|
290
|
+
DVTSecureSocketProxyService.BROADCAST_CHANNEL,
|
|
291
|
+
new ChannelFragmenter(),
|
|
292
|
+
);
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
/**
|
|
296
|
+
* Perform DTX protocol handshake to establish connection and retrieve capabilities
|
|
297
|
+
*/
|
|
298
|
+
private async performHandshake(): Promise<void> {
|
|
299
|
+
const args = new MessageAux();
|
|
300
|
+
args.appendObj({
|
|
301
|
+
'com.apple.private.DTXBlockCompression': 0,
|
|
302
|
+
'com.apple.private.DTXConnection': 1,
|
|
303
|
+
});
|
|
304
|
+
await this.sendMessage(0, '_notifyOfPublishedCapabilities:', args, false);
|
|
305
|
+
|
|
306
|
+
const [retData, aux] = await this.recvMessage();
|
|
307
|
+
const ret = retData ? parseBinaryPlist(retData) : null;
|
|
308
|
+
|
|
309
|
+
const selectorName = this.extractSelectorFromResponse(ret);
|
|
310
|
+
if (selectorName !== '_notifyOfPublishedCapabilities:') {
|
|
311
|
+
throw new Error(`Invalid handshake response selector: ${selectorName}`);
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
if (!aux || aux.length === 0) {
|
|
315
|
+
throw new Error('Invalid handshake response: missing capabilities');
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
// Extract server capabilities from auxiliary data
|
|
319
|
+
this.supportedIdentifiers = this.extractCapabilitiesFromAuxData(aux[0]);
|
|
320
|
+
|
|
321
|
+
this.isHandshakeComplete = true;
|
|
322
|
+
|
|
323
|
+
log.debug(
|
|
324
|
+
`DVT handshake complete. Found ${Object.keys(this.supportedIdentifiers).length} supported identifiers`,
|
|
325
|
+
);
|
|
326
|
+
|
|
327
|
+
// Consume any additional messages buffered after handshake
|
|
328
|
+
await this.drainBufferedMessages();
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
private extractSelectorFromResponse(ret: any): string {
|
|
332
|
+
if (typeof ret === 'string') {
|
|
333
|
+
return ret;
|
|
334
|
+
}
|
|
335
|
+
const objects = extractNSKeyedArchiverObjects(ret);
|
|
336
|
+
if (objects) {
|
|
337
|
+
return objects[1];
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
throw new Error('Invalid handshake response');
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
private extractCapabilitiesFromAuxData(
|
|
344
|
+
capabilitiesData: any,
|
|
345
|
+
): PlistDictionary {
|
|
346
|
+
const objects = extractNSKeyedArchiverObjects(capabilitiesData);
|
|
347
|
+
if (!objects) {
|
|
348
|
+
return capabilitiesData || {};
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
const dictObj = objects[1];
|
|
352
|
+
|
|
353
|
+
if (isNSDictionaryFormat(dictObj)) {
|
|
354
|
+
return extractNSDictionary(dictObj, objects);
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
return extractCapabilityStrings(objects);
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
/**
|
|
361
|
+
* Drain any buffered messages that arrived during handshake
|
|
362
|
+
*/
|
|
363
|
+
private async drainBufferedMessages(): Promise<void> {
|
|
364
|
+
if (this.readBuffer.length === 0) {
|
|
365
|
+
return;
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
try {
|
|
369
|
+
while (this.readBuffer.length >= DTX_CONSTANTS.MESSAGE_HEADER_SIZE) {
|
|
370
|
+
const headerData = this.readBuffer.subarray(
|
|
371
|
+
0,
|
|
372
|
+
DTX_CONSTANTS.MESSAGE_HEADER_SIZE,
|
|
373
|
+
);
|
|
374
|
+
const header = DTXMessage.parseMessageHeader(headerData);
|
|
375
|
+
|
|
376
|
+
const totalSize = DTX_CONSTANTS.MESSAGE_HEADER_SIZE + header.length;
|
|
377
|
+
if (this.readBuffer.length >= totalSize) {
|
|
378
|
+
// Consume complete buffered message
|
|
379
|
+
this.readBuffer = this.readBuffer.subarray(
|
|
380
|
+
DTX_CONSTANTS.MESSAGE_HEADER_SIZE,
|
|
381
|
+
);
|
|
382
|
+
this.readBuffer = this.readBuffer.subarray(header.length);
|
|
383
|
+
} else {
|
|
384
|
+
break;
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
} catch (error) {
|
|
388
|
+
log.debug('Error while draining buffer:', error);
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
/**
|
|
393
|
+
* Receive packet fragments until a complete message is available for the specified channel
|
|
394
|
+
*/
|
|
395
|
+
private async recvPacketFragments(channel: number): Promise<Buffer> {
|
|
396
|
+
while (true) {
|
|
397
|
+
const fragmenter = this.channelMessages.get(channel);
|
|
398
|
+
if (!fragmenter) {
|
|
399
|
+
throw new Error(`No fragmenter for channel ${channel}`);
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
// Check if we have a complete message
|
|
403
|
+
const message = fragmenter.get();
|
|
404
|
+
if (message) {
|
|
405
|
+
return message;
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
// Read next message header
|
|
409
|
+
const headerData = await this.readExact(
|
|
410
|
+
DTX_CONSTANTS.MESSAGE_HEADER_SIZE,
|
|
411
|
+
);
|
|
412
|
+
const header = DTXMessage.parseMessageHeader(headerData);
|
|
413
|
+
|
|
414
|
+
const receivedChannel = Math.abs(header.channelCode);
|
|
415
|
+
|
|
416
|
+
if (!this.channelMessages.has(receivedChannel)) {
|
|
417
|
+
this.channelMessages.set(receivedChannel, new ChannelFragmenter());
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
// Update message ID tracker
|
|
421
|
+
if (!header.conversationIndex && header.identifier > this.curMessageId) {
|
|
422
|
+
this.curMessageId = header.identifier;
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
// Skip first fragment header for multi-fragment messages
|
|
426
|
+
if (header.fragmentCount > 1 && header.fragmentId === 0) {
|
|
427
|
+
continue;
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
// Read message payload
|
|
431
|
+
const messageData = await this.readExact(header.length);
|
|
432
|
+
|
|
433
|
+
// Add fragment to appropriate channel
|
|
434
|
+
const targetFragmenter = this.channelMessages.get(receivedChannel)!;
|
|
435
|
+
targetFragmenter.addFragment(header, messageData);
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
/**
|
|
440
|
+
* Read exact number of bytes from socket with buffering
|
|
441
|
+
*/
|
|
442
|
+
private async readExact(length: number): Promise<Buffer> {
|
|
443
|
+
if (!this.socket) {
|
|
444
|
+
throw new Error(
|
|
445
|
+
`${this.constructor.name} is not initialized. Call connect() before sending messages.`,
|
|
446
|
+
);
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
// Keep reading until we have enough data
|
|
450
|
+
while (this.readBuffer.length < length) {
|
|
451
|
+
const chunk = await new Promise<Buffer>((resolve, reject) => {
|
|
452
|
+
const onData = (data: Buffer) => {
|
|
453
|
+
this.socket!.off('data', onData);
|
|
454
|
+
this.socket!.off('error', onError);
|
|
455
|
+
resolve(data);
|
|
456
|
+
};
|
|
457
|
+
|
|
458
|
+
const onError = (err: Error) => {
|
|
459
|
+
this.socket!.off('data', onData);
|
|
460
|
+
this.socket!.off('error', onError);
|
|
461
|
+
reject(err);
|
|
462
|
+
};
|
|
463
|
+
|
|
464
|
+
this.socket!.once('data', onData);
|
|
465
|
+
this.socket!.once('error', onError);
|
|
466
|
+
});
|
|
467
|
+
|
|
468
|
+
this.readBuffer = Buffer.concat([this.readBuffer, chunk]);
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
// Extract exact amount requested
|
|
472
|
+
const result = this.readBuffer.subarray(0, length);
|
|
473
|
+
this.readBuffer = this.readBuffer.subarray(length);
|
|
474
|
+
|
|
475
|
+
return result;
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
/**
|
|
479
|
+
* Check if response contains an NSError and throw if present
|
|
480
|
+
*/
|
|
481
|
+
private checkForNSError(response: any, context: string): void {
|
|
482
|
+
if (!response || typeof response !== 'object') {
|
|
483
|
+
return;
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
// Check NSKeyedArchiver format
|
|
487
|
+
const objects = extractNSKeyedArchiverObjects(response);
|
|
488
|
+
if (objects) {
|
|
489
|
+
// Check for NSError indicators in $objects
|
|
490
|
+
const hasNSError = objects.some((o) => hasNSErrorIndicators(o));
|
|
491
|
+
|
|
492
|
+
if (hasNSError) {
|
|
493
|
+
const errorMsg =
|
|
494
|
+
objects.find(
|
|
495
|
+
(o: any) =>
|
|
496
|
+
typeof o === 'string' && o.length > MIN_ERROR_DESCRIPTION_LENGTH,
|
|
497
|
+
) || 'Unknown error';
|
|
498
|
+
throw new Error(`${context}: ${errorMsg}`);
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
// Check direct NSError format
|
|
503
|
+
if (hasNSErrorIndicators(response)) {
|
|
504
|
+
throw new Error(`${context}: ${JSON.stringify(response)}`);
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
/**
|
|
509
|
+
* Archive a value using NSKeyedArchiver format for DTX protocol
|
|
510
|
+
*/
|
|
511
|
+
private archiveValue(value: any): Buffer {
|
|
512
|
+
const archived = {
|
|
513
|
+
$version: 100000,
|
|
514
|
+
$archiver: 'NSKeyedArchiver',
|
|
515
|
+
$top: { root: new PlistUID(1) },
|
|
516
|
+
$objects: ['$null', value],
|
|
517
|
+
};
|
|
518
|
+
|
|
519
|
+
return createBinaryPlist(archived);
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
/**
|
|
523
|
+
* Archive a selector string for DTX messages
|
|
524
|
+
*/
|
|
525
|
+
private archiveSelector(selector: string): Buffer {
|
|
526
|
+
return this.archiveValue(selector);
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
/**
|
|
530
|
+
* Build auxiliary data buffer with NSKeyedArchiver encoding for objects
|
|
531
|
+
*/
|
|
532
|
+
private buildAuxiliaryData(args: MessageAux): Buffer {
|
|
533
|
+
const values = args.getValues();
|
|
534
|
+
|
|
535
|
+
if (values.length === 0) {
|
|
536
|
+
return Buffer.alloc(0);
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
const itemBuffers: Buffer[] = [];
|
|
540
|
+
|
|
541
|
+
for (const auxValue of values) {
|
|
542
|
+
// Empty dictionary marker
|
|
543
|
+
const dictMarker = Buffer.alloc(4);
|
|
544
|
+
dictMarker.writeUInt32LE(DTX_CONSTANTS.EMPTY_DICTIONARY, 0);
|
|
545
|
+
itemBuffers.push(dictMarker);
|
|
546
|
+
|
|
547
|
+
// Type marker
|
|
548
|
+
const typeBuffer = Buffer.alloc(4);
|
|
549
|
+
typeBuffer.writeUInt32LE(auxValue.type, 0);
|
|
550
|
+
itemBuffers.push(typeBuffer);
|
|
551
|
+
|
|
552
|
+
// Value data
|
|
553
|
+
switch (auxValue.type) {
|
|
554
|
+
case DTX_CONSTANTS.AUX_TYPE_INT32: {
|
|
555
|
+
const valueBuffer = Buffer.alloc(4);
|
|
556
|
+
valueBuffer.writeUInt32LE(auxValue.value, 0);
|
|
557
|
+
itemBuffers.push(valueBuffer);
|
|
558
|
+
break;
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
case DTX_CONSTANTS.AUX_TYPE_INT64: {
|
|
562
|
+
const valueBuffer = Buffer.alloc(8);
|
|
563
|
+
valueBuffer.writeBigUInt64LE(BigInt(auxValue.value), 0);
|
|
564
|
+
itemBuffers.push(valueBuffer);
|
|
565
|
+
break;
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
case DTX_CONSTANTS.AUX_TYPE_OBJECT: {
|
|
569
|
+
const encodedPlist = this.archiveValue(auxValue.value);
|
|
570
|
+
const lengthBuffer = Buffer.alloc(4);
|
|
571
|
+
lengthBuffer.writeUInt32LE(encodedPlist.length, 0);
|
|
572
|
+
itemBuffers.push(lengthBuffer);
|
|
573
|
+
itemBuffers.push(encodedPlist);
|
|
574
|
+
break;
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
default:
|
|
578
|
+
throw new Error(`Unsupported auxiliary type: ${auxValue.type}`);
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
const itemsData = Buffer.concat(itemBuffers);
|
|
583
|
+
|
|
584
|
+
// Build header: magic + total size of items
|
|
585
|
+
const header = Buffer.alloc(16);
|
|
586
|
+
header.writeBigUInt64LE(BigInt(DTX_CONSTANTS.MESSAGE_AUX_MAGIC), 0);
|
|
587
|
+
header.writeBigUInt64LE(BigInt(itemsData.length), 8);
|
|
588
|
+
|
|
589
|
+
return Buffer.concat([header, itemsData]);
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
/**
|
|
593
|
+
* Parse auxiliary data from buffer
|
|
594
|
+
*
|
|
595
|
+
* The auxiliary data format can be:
|
|
596
|
+
* 1. Standard format: [magic:8][size:8][items...]
|
|
597
|
+
* 2. NSKeyedArchiver bplist format (for handshake responses)
|
|
598
|
+
*/
|
|
599
|
+
private parseAuxiliaryData(buffer: Buffer): any[] {
|
|
600
|
+
if (buffer.length < 16) {
|
|
601
|
+
return [];
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
const magic = buffer.readBigUInt64LE(0);
|
|
605
|
+
|
|
606
|
+
// Check if this is NSKeyedArchiver bplist format (handshake response)
|
|
607
|
+
if (magic !== BigInt(DTX_CONSTANTS.MESSAGE_AUX_MAGIC)) {
|
|
608
|
+
return this.parseAuxiliaryAsBplist(buffer);
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
// Standard auxiliary format
|
|
612
|
+
return this.parseAuxiliaryStandard(buffer);
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
/**
|
|
616
|
+
* Parse auxiliary data in NSKeyedArchiver bplist format
|
|
617
|
+
*/
|
|
618
|
+
private parseAuxiliaryAsBplist(buffer: Buffer): any[] {
|
|
619
|
+
// Find bplist header in buffer
|
|
620
|
+
const bplistMagic = 'bplist00';
|
|
621
|
+
for (let i = 0; i < Math.min(100, buffer.length - 8); i++) {
|
|
622
|
+
if (buffer.toString('ascii', i, i + 8) === bplistMagic) {
|
|
623
|
+
try {
|
|
624
|
+
const plistBuffer = buffer.subarray(i);
|
|
625
|
+
const parsed = parseBinaryPlist(plistBuffer);
|
|
626
|
+
return Array.isArray(parsed) ? parsed : [parsed];
|
|
627
|
+
} catch (error) {
|
|
628
|
+
log.warn('Failed to parse auxiliary bplist:', error);
|
|
629
|
+
}
|
|
630
|
+
break;
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
return [];
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
/**
|
|
637
|
+
* Parse auxiliary data in standard DTX format
|
|
638
|
+
*/
|
|
639
|
+
private parseAuxiliaryStandard(buffer: Buffer): any[] {
|
|
640
|
+
const values: any[] = [];
|
|
641
|
+
let offset = 16; // Skip magic (8) + size (8)
|
|
642
|
+
|
|
643
|
+
const totalSize = buffer.readBigUInt64LE(8);
|
|
644
|
+
const endOffset = offset + Number(totalSize);
|
|
645
|
+
|
|
646
|
+
while (offset < endOffset && offset < buffer.length) {
|
|
647
|
+
// Read and validate empty dictionary marker
|
|
648
|
+
const marker = buffer.readUInt32LE(offset);
|
|
649
|
+
offset += 4;
|
|
650
|
+
|
|
651
|
+
if (marker !== DTX_CONSTANTS.EMPTY_DICTIONARY) {
|
|
652
|
+
offset -= 4; // Rewind if not the expected marker
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
// Read type
|
|
656
|
+
const type = buffer.readUInt32LE(offset);
|
|
657
|
+
offset += 4;
|
|
658
|
+
|
|
659
|
+
// Read value based on type
|
|
660
|
+
try {
|
|
661
|
+
const value = this.parseAuxiliaryValue(buffer, type, offset);
|
|
662
|
+
values.push(value.data);
|
|
663
|
+
offset = value.newOffset;
|
|
664
|
+
} catch (error) {
|
|
665
|
+
log.warn(`Failed to parse auxiliary value at offset ${offset}:`, error);
|
|
666
|
+
break;
|
|
667
|
+
}
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
return values;
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
/**
|
|
674
|
+
* Parse a single auxiliary value
|
|
675
|
+
*/
|
|
676
|
+
private parseAuxiliaryValue(
|
|
677
|
+
buffer: Buffer,
|
|
678
|
+
type: number,
|
|
679
|
+
offset: number,
|
|
680
|
+
): { data: any; newOffset: number } {
|
|
681
|
+
switch (type) {
|
|
682
|
+
case DTX_CONSTANTS.AUX_TYPE_INT32:
|
|
683
|
+
return {
|
|
684
|
+
data: buffer.readUInt32LE(offset),
|
|
685
|
+
newOffset: offset + 4,
|
|
686
|
+
};
|
|
687
|
+
|
|
688
|
+
case DTX_CONSTANTS.AUX_TYPE_INT64:
|
|
689
|
+
return {
|
|
690
|
+
data: buffer.readBigUInt64LE(offset),
|
|
691
|
+
newOffset: offset + 8,
|
|
692
|
+
};
|
|
693
|
+
|
|
694
|
+
case DTX_CONSTANTS.AUX_TYPE_OBJECT: {
|
|
695
|
+
const length = buffer.readUInt32LE(offset);
|
|
696
|
+
const plistData = buffer.subarray(offset + 4, offset + 4 + length);
|
|
697
|
+
|
|
698
|
+
let parsed: any;
|
|
699
|
+
try {
|
|
700
|
+
parsed = parseBinaryPlist(plistData);
|
|
701
|
+
} catch (error) {
|
|
702
|
+
log.warn('Failed to parse auxiliary object plist:', error);
|
|
703
|
+
parsed = plistData;
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
return {
|
|
707
|
+
data: parsed,
|
|
708
|
+
newOffset: offset + 4 + length,
|
|
709
|
+
};
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
default:
|
|
713
|
+
throw new Error(`Unknown auxiliary type: ${type}`);
|
|
714
|
+
}
|
|
715
|
+
}
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
export { Channel, ChannelFragmenter, DTXMessage, MessageAux, DTX_CONSTANTS };
|
|
719
|
+
export {
|
|
720
|
+
decodeNSKeyedArchiver,
|
|
721
|
+
NSKeyedArchiverDecoder,
|
|
722
|
+
} from './nskeyedarchiver-decoder.js';
|
|
723
|
+
export type {
|
|
724
|
+
DTXMessageHeader,
|
|
725
|
+
DTXMessagePayloadHeader,
|
|
726
|
+
MessageAuxValue,
|
|
727
|
+
} from './dtx-message.js';
|