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,468 @@
|
|
|
1
|
+
import { logger } from '@appium/support';
|
|
2
|
+
import { Socket } from 'node:net';
|
|
3
|
+
import tls, { type ConnectionOptions, TLSSocket } from 'tls';
|
|
4
|
+
|
|
5
|
+
import { BasePlistService } from '../../base-plist-service.js';
|
|
6
|
+
import { type PairRecord } from '../pair-record/index.js';
|
|
7
|
+
import { PlistService } from '../plist/plist-service.js';
|
|
8
|
+
import type { PlistMessage, PlistValue } from '../types.js';
|
|
9
|
+
import { RelayService, createUsbmux } from '../usbmux/index.js';
|
|
10
|
+
|
|
11
|
+
const log = logger.getLogger('Lockdown');
|
|
12
|
+
|
|
13
|
+
// Constants
|
|
14
|
+
const LABEL = 'appium-internal';
|
|
15
|
+
const DEFAULT_TIMEOUT = 5000;
|
|
16
|
+
const DEFAULT_LOCKDOWN_PORT = 62078;
|
|
17
|
+
const DEFAULT_RELAY_PORT = 2222;
|
|
18
|
+
|
|
19
|
+
// Types and Interfaces
|
|
20
|
+
interface DeviceProperties {
|
|
21
|
+
ConnectionSpeed: number;
|
|
22
|
+
ConnectionType: string;
|
|
23
|
+
DeviceID: number;
|
|
24
|
+
LocationID: number;
|
|
25
|
+
ProductID: number;
|
|
26
|
+
SerialNumber: string;
|
|
27
|
+
USBSerialNumber: string;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
interface Device {
|
|
31
|
+
DeviceID: number;
|
|
32
|
+
MessageType: string;
|
|
33
|
+
Properties: DeviceProperties;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
interface LockdownServiceInfo {
|
|
37
|
+
lockdownService: LockdownService;
|
|
38
|
+
device: Device;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
interface SessionInfo {
|
|
42
|
+
sessionID: string;
|
|
43
|
+
enableSessionSSL: boolean;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
interface StartSessionRequest {
|
|
47
|
+
Label: string;
|
|
48
|
+
Request: string;
|
|
49
|
+
HostID: string;
|
|
50
|
+
SystemBUID: string;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
interface StartSessionResponse {
|
|
54
|
+
Request?: string;
|
|
55
|
+
SessionID?: PlistValue;
|
|
56
|
+
EnableSessionSSL?: boolean;
|
|
57
|
+
[key: string]: PlistValue | undefined;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
interface TLSConfig {
|
|
61
|
+
cert: string;
|
|
62
|
+
key: string;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Error classes for better error handling
|
|
66
|
+
class LockdownError extends Error {
|
|
67
|
+
constructor(message: string) {
|
|
68
|
+
super(message);
|
|
69
|
+
this.name = 'LockdownError';
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
class TLSUpgradeError extends Error {
|
|
74
|
+
constructor(message: string) {
|
|
75
|
+
super(message);
|
|
76
|
+
this.name = 'TLSUpgradeError';
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
class DeviceNotFoundError extends Error {
|
|
81
|
+
constructor(udid: string) {
|
|
82
|
+
super(`Device with UDID ${udid} not found`);
|
|
83
|
+
this.name = 'DeviceNotFoundError';
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// TLS Manager for handling TLS operations
|
|
88
|
+
class TLSManager {
|
|
89
|
+
private readonly log = logger.getLogger('TLSManager');
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Upgrades a socket to TLS
|
|
93
|
+
*/
|
|
94
|
+
async upgradeSocketToTLS(
|
|
95
|
+
socket: Socket,
|
|
96
|
+
tlsOptions: Partial<ConnectionOptions> = {},
|
|
97
|
+
): Promise<TLSSocket> {
|
|
98
|
+
return new Promise((resolve, reject) => {
|
|
99
|
+
socket.pause();
|
|
100
|
+
this.log.debug('Upgrading socket to TLS...');
|
|
101
|
+
|
|
102
|
+
const secure = tls.connect(
|
|
103
|
+
{
|
|
104
|
+
socket,
|
|
105
|
+
rejectUnauthorized: false,
|
|
106
|
+
minVersion: 'TLSv1.2',
|
|
107
|
+
...tlsOptions,
|
|
108
|
+
},
|
|
109
|
+
() => {
|
|
110
|
+
this.log.info('TLS handshake completed');
|
|
111
|
+
resolve(secure);
|
|
112
|
+
},
|
|
113
|
+
);
|
|
114
|
+
|
|
115
|
+
secure.on('error', (err) => {
|
|
116
|
+
this.log.error(`TLS socket error: ${err}`);
|
|
117
|
+
reject(new TLSUpgradeError(`TLS socket error: ${err.message}`));
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
socket.on('error', (err) => {
|
|
121
|
+
this.log.error(`Underlying socket error during TLS: ${err}`);
|
|
122
|
+
reject(new TLSUpgradeError(`Socket error during TLS: ${err.message}`));
|
|
123
|
+
});
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// Device Manager for handling device operations
|
|
129
|
+
class DeviceManager {
|
|
130
|
+
private readonly log = logger.getLogger('DeviceManager');
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Lists all connected devices
|
|
134
|
+
*/
|
|
135
|
+
async listDevices(): Promise<Device[]> {
|
|
136
|
+
const usbmux = await createUsbmux();
|
|
137
|
+
try {
|
|
138
|
+
this.log.debug('Listing connected devices...');
|
|
139
|
+
const devices = await usbmux.listDevices();
|
|
140
|
+
this.log.debug(
|
|
141
|
+
`Found ${devices.length} devices: ${devices.map((d) => d.Properties.SerialNumber).join(', ')}`,
|
|
142
|
+
);
|
|
143
|
+
return devices;
|
|
144
|
+
} finally {
|
|
145
|
+
await this.closeUsbmux(usbmux);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Finds a device by UDID
|
|
151
|
+
*/
|
|
152
|
+
async findDeviceByUDID(udid: string): Promise<Device> {
|
|
153
|
+
const devices = await this.listDevices();
|
|
154
|
+
|
|
155
|
+
if (!devices || devices.length === 0) {
|
|
156
|
+
throw new LockdownError('No devices connected');
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
const device = devices.find((d) => d.Properties.SerialNumber === udid);
|
|
160
|
+
if (!device) {
|
|
161
|
+
throw new DeviceNotFoundError(udid);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
this.log.info(
|
|
165
|
+
`Found device: DeviceID=${device.DeviceID}, SerialNumber=${device.Properties.SerialNumber}, ConnectionType=${device.Properties.ConnectionType}`,
|
|
166
|
+
);
|
|
167
|
+
|
|
168
|
+
return device;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Reads pair record for a device
|
|
173
|
+
*/
|
|
174
|
+
async readPairRecord(udid: string): Promise<PairRecord> {
|
|
175
|
+
this.log.debug(`Retrieving pair record for UDID: ${udid}`);
|
|
176
|
+
const usbmux = await createUsbmux();
|
|
177
|
+
|
|
178
|
+
try {
|
|
179
|
+
const record = await usbmux.readPairRecord(udid);
|
|
180
|
+
|
|
181
|
+
if (!record?.HostCertificate || !record.HostPrivateKey) {
|
|
182
|
+
throw new LockdownError('Pair record missing certificate or key');
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
this.log.info('Pair record retrieved successfully');
|
|
186
|
+
return record;
|
|
187
|
+
} catch (err) {
|
|
188
|
+
this.log.error(`Error getting pair record: ${err}`);
|
|
189
|
+
throw err;
|
|
190
|
+
} finally {
|
|
191
|
+
await this.closeUsbmux(usbmux);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
private async closeUsbmux(usbmux: any): Promise<void> {
|
|
196
|
+
try {
|
|
197
|
+
await usbmux.close();
|
|
198
|
+
} catch (err) {
|
|
199
|
+
this.log.error(`Error closing usbmux: ${err}`);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// Main LockdownService class
|
|
205
|
+
export class LockdownService extends BasePlistService {
|
|
206
|
+
private readonly udid: string;
|
|
207
|
+
private tlsService?: PlistService;
|
|
208
|
+
private isTLS = false;
|
|
209
|
+
private tlsUpgradePromise?: Promise<void>;
|
|
210
|
+
private _relayService?: RelayService;
|
|
211
|
+
private readonly tlsManager = new TLSManager();
|
|
212
|
+
private readonly deviceManager = new DeviceManager();
|
|
213
|
+
|
|
214
|
+
constructor(socket: Socket, udid: string, autoSecure = true) {
|
|
215
|
+
super(socket);
|
|
216
|
+
this.udid = udid;
|
|
217
|
+
log.info(`LockdownService initialized for UDID: ${udid}`);
|
|
218
|
+
|
|
219
|
+
if (autoSecure) {
|
|
220
|
+
this.tlsUpgradePromise = this.tryUpgradeToTLS().catch((err) =>
|
|
221
|
+
log.warn(`Auto TLS upgrade failed: ${err.message}`),
|
|
222
|
+
);
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* Starts a lockdown session
|
|
228
|
+
*/
|
|
229
|
+
async startSession(
|
|
230
|
+
hostID: string,
|
|
231
|
+
systemBUID: string,
|
|
232
|
+
timeout = DEFAULT_TIMEOUT,
|
|
233
|
+
): Promise<SessionInfo> {
|
|
234
|
+
log.debug(`Starting lockdown session with HostID: ${hostID}`);
|
|
235
|
+
|
|
236
|
+
const request: Record<string, PlistValue> = {
|
|
237
|
+
Label: LABEL,
|
|
238
|
+
Request: 'StartSession',
|
|
239
|
+
HostID: hostID,
|
|
240
|
+
SystemBUID: systemBUID,
|
|
241
|
+
};
|
|
242
|
+
|
|
243
|
+
const response = (await this.sendAndReceive(
|
|
244
|
+
request,
|
|
245
|
+
timeout,
|
|
246
|
+
)) as StartSessionResponse;
|
|
247
|
+
|
|
248
|
+
if (response.Request === 'StartSession' && response.SessionID) {
|
|
249
|
+
const sessionInfo: SessionInfo = {
|
|
250
|
+
sessionID: String(response.SessionID),
|
|
251
|
+
enableSessionSSL: Boolean(response.EnableSessionSSL),
|
|
252
|
+
};
|
|
253
|
+
|
|
254
|
+
log.info(`Lockdown session started, SessionID: ${sessionInfo.sessionID}`);
|
|
255
|
+
return sessionInfo;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
throw new LockdownError(
|
|
259
|
+
`Unexpected session data: ${JSON.stringify(response)}`,
|
|
260
|
+
);
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/**
|
|
264
|
+
* Attempts to upgrade the connection to TLS
|
|
265
|
+
*/
|
|
266
|
+
async tryUpgradeToTLS(): Promise<void> {
|
|
267
|
+
try {
|
|
268
|
+
const pairRecord = await this.deviceManager.readPairRecord(this.udid);
|
|
269
|
+
|
|
270
|
+
if (!this.validatePairRecord(pairRecord)) {
|
|
271
|
+
log.warn('Invalid pair record for TLS upgrade');
|
|
272
|
+
return;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
const sessionInfo = await this.startSession(
|
|
276
|
+
pairRecord.HostID!,
|
|
277
|
+
pairRecord.SystemBUID!,
|
|
278
|
+
);
|
|
279
|
+
|
|
280
|
+
if (!sessionInfo.enableSessionSSL) {
|
|
281
|
+
log.info('Device did not request TLS upgrade. Continuing unencrypted.');
|
|
282
|
+
return;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
await this.performTLSUpgrade(pairRecord);
|
|
286
|
+
} catch (err) {
|
|
287
|
+
log.error(`TLS upgrade failed: ${err}`);
|
|
288
|
+
throw err;
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
/**
|
|
293
|
+
* Gets the current socket (TLS or regular)
|
|
294
|
+
*/
|
|
295
|
+
public getSocket(): Socket | TLSSocket {
|
|
296
|
+
return this.isTLS && this.tlsService
|
|
297
|
+
? this.tlsService.getSocket()
|
|
298
|
+
: this.getPlistService().getSocket();
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
/**
|
|
302
|
+
* Sends a message and receives a response
|
|
303
|
+
*/
|
|
304
|
+
public async sendAndReceive(
|
|
305
|
+
msg: Record<string, PlistValue>,
|
|
306
|
+
timeout = DEFAULT_TIMEOUT,
|
|
307
|
+
): Promise<PlistMessage> {
|
|
308
|
+
const service =
|
|
309
|
+
this.isTLS && this.tlsService ? this.tlsService : this._plistService;
|
|
310
|
+
return service.sendPlistAndReceive(msg, timeout);
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
/**
|
|
314
|
+
* Closes the service and associated resources
|
|
315
|
+
*/
|
|
316
|
+
public close(): void {
|
|
317
|
+
log.info('Closing LockdownService connections');
|
|
318
|
+
|
|
319
|
+
try {
|
|
320
|
+
this.closeSocket();
|
|
321
|
+
this.stopRelayService();
|
|
322
|
+
} catch (err) {
|
|
323
|
+
log.error(`Error during close: ${err}`);
|
|
324
|
+
throw err;
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
/**
|
|
329
|
+
* Sets the relay service for this lockdown instance
|
|
330
|
+
*/
|
|
331
|
+
public set relayService(relay: RelayService) {
|
|
332
|
+
this._relayService = relay;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
/**
|
|
336
|
+
* Gets the relay service for this lockdown instance
|
|
337
|
+
*/
|
|
338
|
+
public get relayService(): RelayService | undefined {
|
|
339
|
+
return this._relayService;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
/**
|
|
343
|
+
* Waits for TLS upgrade to complete if in progress
|
|
344
|
+
*/
|
|
345
|
+
public async waitForTLSUpgrade(): Promise<void> {
|
|
346
|
+
if (this.tlsUpgradePromise) {
|
|
347
|
+
await this.tlsUpgradePromise;
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
/**
|
|
352
|
+
* Stops the relay service with an optional custom message
|
|
353
|
+
*/
|
|
354
|
+
public stopRelayService(
|
|
355
|
+
message = 'Stopping relay server associated with LockdownService',
|
|
356
|
+
): void {
|
|
357
|
+
const relay = this.relayService;
|
|
358
|
+
if (relay) {
|
|
359
|
+
log.info(message);
|
|
360
|
+
(async () => {
|
|
361
|
+
try {
|
|
362
|
+
await relay.stop();
|
|
363
|
+
log.info('Relay server stopped successfully');
|
|
364
|
+
} catch (err) {
|
|
365
|
+
log.error(`Error stopping relay server: ${err}`);
|
|
366
|
+
}
|
|
367
|
+
})();
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
private validatePairRecord(record: PairRecord): boolean {
|
|
372
|
+
return Boolean(
|
|
373
|
+
record?.HostCertificate &&
|
|
374
|
+
record.HostPrivateKey &&
|
|
375
|
+
record.HostID &&
|
|
376
|
+
record.SystemBUID,
|
|
377
|
+
);
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
private async performTLSUpgrade(pairRecord: PairRecord): Promise<void> {
|
|
381
|
+
const tlsConfig: TLSConfig = {
|
|
382
|
+
cert: pairRecord.HostCertificate!,
|
|
383
|
+
key: pairRecord.HostPrivateKey!,
|
|
384
|
+
};
|
|
385
|
+
|
|
386
|
+
const tlsSocket = await this.tlsManager.upgradeSocketToTLS(
|
|
387
|
+
this.getSocket() as Socket,
|
|
388
|
+
tlsConfig,
|
|
389
|
+
);
|
|
390
|
+
|
|
391
|
+
this.tlsService = new PlistService(tlsSocket);
|
|
392
|
+
this.isTLS = true;
|
|
393
|
+
log.info('Successfully upgraded connection to TLS');
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
private closeSocket(): void {
|
|
397
|
+
if (this.isTLS && this.tlsService) {
|
|
398
|
+
this.tlsService.close();
|
|
399
|
+
} else {
|
|
400
|
+
super.close();
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
// Factory class for creating LockdownService instances
|
|
406
|
+
export class LockdownServiceFactory {
|
|
407
|
+
private readonly deviceManager = new DeviceManager();
|
|
408
|
+
|
|
409
|
+
/**
|
|
410
|
+
* Creates a LockdownService for a specific device UDID
|
|
411
|
+
*/
|
|
412
|
+
async createByUDID(
|
|
413
|
+
udid: string,
|
|
414
|
+
port = DEFAULT_LOCKDOWN_PORT,
|
|
415
|
+
autoSecure = true,
|
|
416
|
+
): Promise<LockdownServiceInfo> {
|
|
417
|
+
log.info(`Creating LockdownService for UDID: ${udid}`);
|
|
418
|
+
|
|
419
|
+
// Find the device
|
|
420
|
+
const device = await this.deviceManager.findDeviceByUDID(udid);
|
|
421
|
+
|
|
422
|
+
// Create relay service
|
|
423
|
+
const relay = new RelayService(device.DeviceID, port, DEFAULT_RELAY_PORT);
|
|
424
|
+
await relay.start();
|
|
425
|
+
|
|
426
|
+
let service: LockdownService | undefined;
|
|
427
|
+
try {
|
|
428
|
+
// Connect through the relay
|
|
429
|
+
const socket = await relay.connect();
|
|
430
|
+
log.debug('Socket connected, creating LockdownService');
|
|
431
|
+
|
|
432
|
+
// Create the lockdown service
|
|
433
|
+
service = new LockdownService(socket, udid, autoSecure);
|
|
434
|
+
service.relayService = relay;
|
|
435
|
+
|
|
436
|
+
// Wait for TLS upgrade if enabled
|
|
437
|
+
if (autoSecure) {
|
|
438
|
+
log.debug('Waiting for TLS upgrade to complete...');
|
|
439
|
+
await service.waitForTLSUpgrade();
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
return { lockdownService: service, device };
|
|
443
|
+
} catch (err) {
|
|
444
|
+
// Clean up relay on error
|
|
445
|
+
service?.stopRelayService('Stopping relay after failure');
|
|
446
|
+
throw err;
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
// Export factory function for backward compatibility
|
|
452
|
+
export async function createLockdownServiceByUDID(
|
|
453
|
+
udid: string,
|
|
454
|
+
port = DEFAULT_LOCKDOWN_PORT,
|
|
455
|
+
autoSecure = true,
|
|
456
|
+
): Promise<LockdownServiceInfo> {
|
|
457
|
+
const factory = new LockdownServiceFactory();
|
|
458
|
+
return factory.createByUDID(udid, port, autoSecure);
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
// Export the TLS upgrade function for external use
|
|
462
|
+
export function upgradeSocketToTLS(
|
|
463
|
+
socket: Socket,
|
|
464
|
+
tlsOptions: Partial<ConnectionOptions> = {},
|
|
465
|
+
): Promise<TLSSocket> {
|
|
466
|
+
const tlsManager = new TLSManager();
|
|
467
|
+
return tlsManager.upgradeSocketToTLS(socket, tlsOptions);
|
|
468
|
+
}
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import { logger } from '@appium/support';
|
|
2
|
+
import fs from 'fs';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
|
|
5
|
+
const log = logger.getLogger('PairRecord');
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Interface defining the structure of a pair record.
|
|
9
|
+
*/
|
|
10
|
+
export interface PairRecord {
|
|
11
|
+
HostID: string | null;
|
|
12
|
+
SystemBUID: string | null;
|
|
13
|
+
HostCertificate: string | null;
|
|
14
|
+
HostPrivateKey: string | null;
|
|
15
|
+
DeviceCertificate: string | null;
|
|
16
|
+
RootCertificate: string | null;
|
|
17
|
+
RootPrivateKey: string | null;
|
|
18
|
+
WiFiMACAddress: string | null;
|
|
19
|
+
EscrowBag: string | null;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Interface for the raw response from plist.parsePlist
|
|
24
|
+
*/
|
|
25
|
+
export interface RawPairRecordResponse {
|
|
26
|
+
HostID: string;
|
|
27
|
+
SystemBUID: string;
|
|
28
|
+
HostCertificate: Buffer;
|
|
29
|
+
HostPrivateKey: Buffer;
|
|
30
|
+
DeviceCertificate: Buffer;
|
|
31
|
+
RootCertificate: Buffer;
|
|
32
|
+
RootPrivateKey: Buffer;
|
|
33
|
+
WiFiMACAddress: string;
|
|
34
|
+
EscrowBag: Buffer;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Converts a buffer containing PEM data to a string
|
|
39
|
+
* @param buffer - Buffer containing PEM data
|
|
40
|
+
* @returns String representation of the PEM data
|
|
41
|
+
*/
|
|
42
|
+
function bufferToPEMString(buffer: Buffer): string {
|
|
43
|
+
return buffer.toString('utf8');
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Processes raw response from plist.parsePlist and formats it into a proper pair-record
|
|
48
|
+
* @param response - Response from plist.parsePlist(data.payload.PairRecordData)
|
|
49
|
+
* @returns Formatted pair-record object with properly structured data
|
|
50
|
+
*/
|
|
51
|
+
export function processPlistResponse(
|
|
52
|
+
response: RawPairRecordResponse,
|
|
53
|
+
): PairRecord {
|
|
54
|
+
return {
|
|
55
|
+
HostID: response.HostID || null,
|
|
56
|
+
SystemBUID: response.SystemBUID || null,
|
|
57
|
+
HostCertificate: response.HostCertificate
|
|
58
|
+
? bufferToPEMString(response.HostCertificate)
|
|
59
|
+
: null,
|
|
60
|
+
HostPrivateKey: response.HostPrivateKey
|
|
61
|
+
? bufferToPEMString(response.HostPrivateKey)
|
|
62
|
+
: null,
|
|
63
|
+
DeviceCertificate: response.DeviceCertificate
|
|
64
|
+
? bufferToPEMString(response.DeviceCertificate)
|
|
65
|
+
: null,
|
|
66
|
+
RootCertificate: response.RootCertificate
|
|
67
|
+
? bufferToPEMString(response.RootCertificate)
|
|
68
|
+
: null,
|
|
69
|
+
RootPrivateKey: response.RootPrivateKey
|
|
70
|
+
? bufferToPEMString(response.RootPrivateKey)
|
|
71
|
+
: null,
|
|
72
|
+
WiFiMACAddress: response.WiFiMACAddress || null,
|
|
73
|
+
// For EscrowBag, we need it as a base64 string
|
|
74
|
+
EscrowBag: response.EscrowBag
|
|
75
|
+
? response.EscrowBag.toString('base64')
|
|
76
|
+
: null,
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/* --- File storage functions remain unchanged --- */
|
|
81
|
+
|
|
82
|
+
const RECORDS_DIR = path.join(process.cwd(), '../../.records');
|
|
83
|
+
|
|
84
|
+
async function ensureRecordsDirectoryExists(): Promise<void> {
|
|
85
|
+
await fs.promises.mkdir(RECORDS_DIR, { recursive: true, mode: 0o777 });
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Saves a pair record to the filesystem.
|
|
90
|
+
* @param udid - Device UDID.
|
|
91
|
+
* @param pairRecord - Pair record to save.
|
|
92
|
+
* @returns Promise that resolves when record is saved.
|
|
93
|
+
*/
|
|
94
|
+
export async function savePairRecord(
|
|
95
|
+
udid: string,
|
|
96
|
+
pairRecord: PairRecord,
|
|
97
|
+
): Promise<void> {
|
|
98
|
+
await ensureRecordsDirectoryExists();
|
|
99
|
+
|
|
100
|
+
const recordPath = path.join(RECORDS_DIR, `${udid}-record.json`);
|
|
101
|
+
try {
|
|
102
|
+
await fs.promises.writeFile(
|
|
103
|
+
recordPath,
|
|
104
|
+
JSON.stringify(pairRecord, null, 2),
|
|
105
|
+
{ mode: 0o777 },
|
|
106
|
+
);
|
|
107
|
+
log.info(`Pair record saved: ${recordPath}`);
|
|
108
|
+
} catch (error) {
|
|
109
|
+
log.error(`Failed to save pair record for ${udid}: ${error}`);
|
|
110
|
+
throw error;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Gets a saved pair record from the filesystem.
|
|
116
|
+
* @param udid - Device UDID.
|
|
117
|
+
* @returns Promise that resolves with the pair record or null if not found.
|
|
118
|
+
*/
|
|
119
|
+
export async function getPairRecord(udid: string): Promise<PairRecord | null> {
|
|
120
|
+
const recordPath = path.join(RECORDS_DIR, `${udid}-record.json`);
|
|
121
|
+
|
|
122
|
+
try {
|
|
123
|
+
const data = await fs.promises.readFile(recordPath, 'utf8');
|
|
124
|
+
return JSON.parse(data) as PairRecord;
|
|
125
|
+
} catch (error) {
|
|
126
|
+
if ((error as NodeJS.ErrnoException).code === 'ENOENT') {
|
|
127
|
+
return null;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
log.error(`Failed to read pair record for ${udid}: ${error}`);
|
|
131
|
+
throw error;
|
|
132
|
+
}
|
|
133
|
+
}
|