appium-ios-remotexpc 0.21.1 → 0.22.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 +13 -0
- package/build/src/lib/apple-tv/constants.d.ts +4 -3
- package/build/src/lib/apple-tv/constants.d.ts.map +1 -1
- package/build/src/lib/apple-tv/constants.js +10 -3
- package/build/src/lib/apple-tv/discovery/device-discovery.d.ts.map +1 -1
- package/build/src/lib/apple-tv/discovery/device-discovery.js +2 -2
- package/build/src/lib/apple-tv/encryption/index.d.ts +1 -0
- package/build/src/lib/apple-tv/encryption/index.d.ts.map +1 -1
- package/build/src/lib/apple-tv/encryption/index.js +1 -0
- package/build/src/lib/apple-tv/encryption/x25519.d.ts +8 -0
- package/build/src/lib/apple-tv/encryption/x25519.d.ts.map +1 -0
- package/build/src/lib/apple-tv/encryption/x25519.js +52 -0
- package/build/src/lib/apple-tv/index.d.ts +1 -0
- package/build/src/lib/apple-tv/index.d.ts.map +1 -1
- package/build/src/lib/apple-tv/index.js +1 -0
- package/build/src/lib/apple-tv/network/network-client.js +2 -2
- package/build/src/lib/apple-tv/pairing/user-input-service.d.ts.map +1 -1
- package/build/src/lib/apple-tv/pairing/user-input-service.js +2 -2
- package/build/src/lib/apple-tv/pairing-protocol/constants.d.ts +17 -0
- package/build/src/lib/apple-tv/pairing-protocol/constants.d.ts.map +1 -1
- package/build/src/lib/apple-tv/pairing-protocol/constants.js +25 -0
- package/build/src/lib/apple-tv/pairing-protocol/index.d.ts +2 -1
- package/build/src/lib/apple-tv/pairing-protocol/index.d.ts.map +1 -1
- package/build/src/lib/apple-tv/pairing-protocol/index.js +2 -1
- package/build/src/lib/apple-tv/pairing-protocol/pair-verification-protocol.d.ts +66 -0
- package/build/src/lib/apple-tv/pairing-protocol/pair-verification-protocol.d.ts.map +1 -0
- package/build/src/lib/apple-tv/pairing-protocol/pair-verification-protocol.js +178 -0
- package/build/src/lib/apple-tv/pairing-protocol/pairing-protocol.d.ts +35 -2
- package/build/src/lib/apple-tv/pairing-protocol/pairing-protocol.d.ts.map +1 -1
- package/build/src/lib/apple-tv/pairing-protocol/pairing-protocol.js +48 -14
- package/build/src/lib/apple-tv/storage/index.d.ts +1 -1
- package/build/src/lib/apple-tv/storage/index.d.ts.map +1 -1
- package/build/src/lib/apple-tv/storage/pairing-storage.d.ts +4 -1
- package/build/src/lib/apple-tv/storage/pairing-storage.d.ts.map +1 -1
- package/build/src/lib/apple-tv/storage/pairing-storage.js +59 -4
- package/build/src/lib/apple-tv/storage/types.d.ts +7 -1
- package/build/src/lib/apple-tv/storage/types.d.ts.map +1 -1
- package/build/src/lib/apple-tv/tunnel/index.d.ts +3 -0
- package/build/src/lib/apple-tv/tunnel/index.d.ts.map +1 -0
- package/build/src/lib/apple-tv/tunnel/index.js +1 -0
- package/build/src/lib/apple-tv/tunnel/tunnel-service.d.ts +57 -0
- package/build/src/lib/apple-tv/tunnel/tunnel-service.d.ts.map +1 -0
- package/build/src/lib/apple-tv/tunnel/tunnel-service.js +357 -0
- package/build/src/lib/apple-tv/tunnel/types.d.ts +22 -0
- package/build/src/lib/apple-tv/tunnel/types.d.ts.map +1 -0
- package/build/src/lib/apple-tv/tunnel/types.js +1 -0
- package/build/src/lib/bonjour/bonjour-discovery.d.ts.map +1 -1
- package/build/src/lib/bonjour/bonjour-discovery.js +3 -3
- package/build/src/lib/lockdown/index.d.ts.map +1 -1
- package/build/src/lib/plist/length-based-splitter.d.ts.map +1 -1
- package/build/src/lib/plist/length-based-splitter.js +0 -7
- package/build/src/lib/tunnel/index.d.ts +1 -0
- package/build/src/lib/tunnel/index.d.ts.map +1 -1
- package/build/src/lib/tunnel/packet-stream-server.d.ts.map +1 -1
- package/build/src/lib/tunnel/tunnel-registry-server.d.ts +1 -0
- package/build/src/lib/tunnel/tunnel-registry-server.d.ts.map +1 -1
- package/build/src/lib/tunnel/tunnel-registry-server.js +1 -1
- package/build/src/services/ios/afc/index.d.ts.map +1 -1
- package/build/src/services/ios/afc/index.js +14 -2
- package/build/src/services/ios/afc/stream-utils.d.ts.map +1 -1
- package/build/src/services/ios/afc/stream-utils.js +0 -2
- package/build/src/services/ios/mobile-config/index.js +2 -2
- package/package.json +2 -1
- package/scripts/pair-appletv.ts +2 -2
- package/scripts/start-appletv-tunnel.ts +178 -0
- package/scripts/test-tunnel-creation.ts +32 -23
- package/src/lib/apple-tv/constants.ts +11 -3
- package/src/lib/apple-tv/discovery/device-discovery.ts +2 -3
- package/src/lib/apple-tv/encryption/index.ts +6 -0
- package/src/lib/apple-tv/encryption/x25519.ts +79 -0
- package/src/lib/apple-tv/index.ts +1 -0
- package/src/lib/apple-tv/network/network-client.ts +2 -2
- package/src/lib/apple-tv/pairing/user-input-service.ts +2 -2
- package/src/lib/apple-tv/pairing-protocol/constants.ts +29 -0
- package/src/lib/apple-tv/pairing-protocol/index.ts +12 -1
- package/src/lib/apple-tv/pairing-protocol/pair-verification-protocol.ts +329 -0
- package/src/lib/apple-tv/pairing-protocol/pairing-protocol.ts +49 -19
- package/src/lib/apple-tv/storage/index.ts +1 -1
- package/src/lib/apple-tv/storage/pairing-storage.ts +73 -5
- package/src/lib/apple-tv/storage/types.ts +8 -1
- package/src/lib/apple-tv/tunnel/index.ts +2 -0
- package/src/lib/apple-tv/tunnel/tunnel-service.ts +543 -0
- package/src/lib/apple-tv/tunnel/types.ts +23 -0
- package/src/lib/bonjour/bonjour-discovery.ts +3 -5
- package/src/lib/lockdown/index.ts +0 -7
- package/src/lib/plist/length-based-splitter.ts +0 -22
- package/src/lib/tunnel/index.ts +2 -8
- package/src/lib/tunnel/packet-stream-server.ts +0 -8
- package/src/lib/tunnel/tunnel-registry-server.ts +1 -1
- package/src/services/ios/afc/index.ts +14 -3
- package/src/services/ios/afc/stream-utils.ts +0 -2
- package/src/services/ios/mobile-config/index.ts +2 -2
|
@@ -0,0 +1,543 @@
|
|
|
1
|
+
import { util } from '@appium/support';
|
|
2
|
+
import { lookup } from 'node:dns/promises';
|
|
3
|
+
import * as tls from 'node:tls';
|
|
4
|
+
|
|
5
|
+
import type { AppleTVDevice } from '../../bonjour/bonjour-discovery.js';
|
|
6
|
+
import { BonjourDiscovery } from '../../bonjour/bonjour-discovery.js';
|
|
7
|
+
import { getLogger } from '../../logger.js';
|
|
8
|
+
import { DEFAULT_PAIRING_CONFIG } from '../constants.js';
|
|
9
|
+
import {
|
|
10
|
+
decryptChaCha20Poly1305,
|
|
11
|
+
encryptChaCha20Poly1305,
|
|
12
|
+
} from '../encryption/index.js';
|
|
13
|
+
import { PairingError } from '../errors.js';
|
|
14
|
+
import { NetworkClient } from '../network/index.js';
|
|
15
|
+
import type { NetworkClientInterface } from '../network/types.js';
|
|
16
|
+
import { PairVerificationProtocol } from '../pairing-protocol/index.js';
|
|
17
|
+
import type { VerificationKeys } from '../pairing-protocol/pair-verification-protocol.js';
|
|
18
|
+
import { PairingStorage } from '../storage/pairing-storage.js';
|
|
19
|
+
import type { PairRecord } from '../storage/types.js';
|
|
20
|
+
import type { TcpListenerInfo, TlsPskConnectionOptions } from './types.js';
|
|
21
|
+
|
|
22
|
+
const appleTVLog = getLogger('AppleTVTunnelService');
|
|
23
|
+
|
|
24
|
+
export class TunnelService {
|
|
25
|
+
private static readonly log = getLogger('TunnelService');
|
|
26
|
+
private encryptedSequenceNumber = 0;
|
|
27
|
+
|
|
28
|
+
constructor(
|
|
29
|
+
private readonly networkClient: NetworkClientInterface,
|
|
30
|
+
private readonly keys: VerificationKeys,
|
|
31
|
+
private sequenceNumber: number,
|
|
32
|
+
) {}
|
|
33
|
+
|
|
34
|
+
async createTcpListener(): Promise<TcpListenerInfo> {
|
|
35
|
+
TunnelService.log.debug('Creating TCP listener (Encrypted Request)');
|
|
36
|
+
|
|
37
|
+
const request = {
|
|
38
|
+
request: {
|
|
39
|
+
_0: {
|
|
40
|
+
createListener: {
|
|
41
|
+
key: this.keys.encryptionKey.toString('base64'),
|
|
42
|
+
peerConnectionsInfo: [
|
|
43
|
+
{
|
|
44
|
+
owningPID: process.pid,
|
|
45
|
+
owningProcessName: 'CoreDeviceService',
|
|
46
|
+
},
|
|
47
|
+
],
|
|
48
|
+
transportProtocolType: 'tcp',
|
|
49
|
+
},
|
|
50
|
+
},
|
|
51
|
+
},
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
const nonce = Buffer.alloc(12);
|
|
55
|
+
nonce.writeBigUInt64LE(BigInt(this.encryptedSequenceNumber), 0);
|
|
56
|
+
|
|
57
|
+
const requestJson = JSON.stringify(request);
|
|
58
|
+
|
|
59
|
+
const encrypted = encryptChaCha20Poly1305({
|
|
60
|
+
plaintext: Buffer.from(requestJson, 'utf8'),
|
|
61
|
+
key: this.keys.clientEncryptionKey,
|
|
62
|
+
nonce,
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
const encryptedPayload = {
|
|
66
|
+
message: {
|
|
67
|
+
streamEncrypted: {
|
|
68
|
+
_0: encrypted.toString('base64'),
|
|
69
|
+
},
|
|
70
|
+
},
|
|
71
|
+
originatedBy: 'host',
|
|
72
|
+
sequenceNumber: this.sequenceNumber++,
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
await this.networkClient.sendPacket(encryptedPayload);
|
|
76
|
+
this.encryptedSequenceNumber++;
|
|
77
|
+
|
|
78
|
+
const response = await this.networkClient.receiveResponse();
|
|
79
|
+
|
|
80
|
+
const encryptedData = response.message?.streamEncrypted?._0;
|
|
81
|
+
|
|
82
|
+
if (!encryptedData) {
|
|
83
|
+
throw new PairingError(
|
|
84
|
+
'Failed to receive encrypted response from device',
|
|
85
|
+
'ENCRYPTED_RESPONSE_MISSING',
|
|
86
|
+
{ response },
|
|
87
|
+
);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const responseNonce = Buffer.alloc(12);
|
|
91
|
+
responseNonce.writeBigUInt64LE(BigInt(this.encryptedSequenceNumber - 1), 0);
|
|
92
|
+
|
|
93
|
+
const decrypted = decryptChaCha20Poly1305({
|
|
94
|
+
ciphertext: Buffer.from(encryptedData, 'base64'),
|
|
95
|
+
key: this.keys.serverEncryptionKey,
|
|
96
|
+
nonce: responseNonce,
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
const responseJson = JSON.parse(decrypted.toString('utf8'));
|
|
100
|
+
const createListenerResponse = responseJson?.response?._1?.createListener;
|
|
101
|
+
|
|
102
|
+
if (!createListenerResponse?.port) {
|
|
103
|
+
TunnelService.log.error('Invalid createListener response:', responseJson);
|
|
104
|
+
throw new PairingError(
|
|
105
|
+
'TCP listener creation failed: missing port in response',
|
|
106
|
+
'LISTENER_PORT_MISSING',
|
|
107
|
+
{ response: responseJson },
|
|
108
|
+
);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
TunnelService.log.debug(
|
|
112
|
+
`TCP Listener created on port: ${createListenerResponse.port}`,
|
|
113
|
+
);
|
|
114
|
+
|
|
115
|
+
return createListenerResponse;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
async createTlsPskConnection(
|
|
119
|
+
hostname: string,
|
|
120
|
+
port: number,
|
|
121
|
+
): Promise<tls.TLSSocket> {
|
|
122
|
+
TunnelService.log.debug(
|
|
123
|
+
`Creating TLS-PSK connection to ${hostname}:${port}`,
|
|
124
|
+
);
|
|
125
|
+
|
|
126
|
+
return new Promise((resolve, reject) => {
|
|
127
|
+
const options: TlsPskConnectionOptions = {
|
|
128
|
+
host: hostname,
|
|
129
|
+
port,
|
|
130
|
+
pskCallback: (hint: string | null) => {
|
|
131
|
+
TunnelService.log.debug(`PSK callback invoked with hint: ${hint}`);
|
|
132
|
+
return {
|
|
133
|
+
psk: this.keys.encryptionKey,
|
|
134
|
+
identity: '',
|
|
135
|
+
};
|
|
136
|
+
},
|
|
137
|
+
ciphers:
|
|
138
|
+
'PSK-AES256-CBC-SHA:PSK-AES128-CBC-SHA:PSK-3DES-EDE-CBC-SHA:PSK-RC4-SHA:PSK',
|
|
139
|
+
secureProtocol: 'TLSv1_2_method',
|
|
140
|
+
// SECURITY NOTE: Disabling certificate validation is intentional and safe in this context.
|
|
141
|
+
// This connection uses TLS-PSK (Pre-Shared Key) authentication, where the pre-shared key
|
|
142
|
+
// itself provides mutual authentication between client and server. Traditional X.509
|
|
143
|
+
// certificate validation is not used in PSK-based TLS connections. The encryption key
|
|
144
|
+
// was securely established during the pairing process (which involves PIN verification),
|
|
145
|
+
// and this key authenticates both parties. This is the standard approach for Apple TV's
|
|
146
|
+
// RemoteXPC protocol and should NOT be changed to use certificate validation.
|
|
147
|
+
rejectUnauthorized: false,
|
|
148
|
+
checkServerIdentity: () => undefined,
|
|
149
|
+
};
|
|
150
|
+
|
|
151
|
+
const socket = tls.connect(options, () => {
|
|
152
|
+
TunnelService.log.debug('TLS-PSK connection established');
|
|
153
|
+
resolve(socket);
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
socket.on('error', (error: Error & { code?: string }) => {
|
|
157
|
+
TunnelService.log.error('TLS-PSK connection error:', error);
|
|
158
|
+
|
|
159
|
+
if (
|
|
160
|
+
error.message?.includes('no shared cipher') ||
|
|
161
|
+
error.code === 'ECONNRESET'
|
|
162
|
+
) {
|
|
163
|
+
TunnelService.log.error(
|
|
164
|
+
'PSK ciphers may not be available in your Node.js build',
|
|
165
|
+
);
|
|
166
|
+
TunnelService.log.error('You may need to:');
|
|
167
|
+
TunnelService.log.error(
|
|
168
|
+
'1. Use Node.js compiled with PSK-enabled OpenSSL',
|
|
169
|
+
);
|
|
170
|
+
TunnelService.log.error(
|
|
171
|
+
'2. Use a Python subprocess for the TLS-PSK connection',
|
|
172
|
+
);
|
|
173
|
+
TunnelService.log.error('3. Use a native module like node-openssl');
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
reject(error);
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
socket.on('secureConnect', () => {
|
|
180
|
+
TunnelService.log.debug('Secure connection event fired');
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
socket.on('tlsClientError', (error) => {
|
|
184
|
+
TunnelService.log.error('TLS client error:', error);
|
|
185
|
+
});
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
getSequenceNumber(): number {
|
|
190
|
+
return this.sequenceNumber;
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* High-level service for establishing Apple TV tunnels.
|
|
196
|
+
* Orchestrates device discovery, pairing verification, and tunnel creation.
|
|
197
|
+
*/
|
|
198
|
+
export class AppleTVTunnelService {
|
|
199
|
+
private readonly networkClient: NetworkClient;
|
|
200
|
+
private readonly storage: PairingStorage;
|
|
201
|
+
private sequenceNumber = 0;
|
|
202
|
+
|
|
203
|
+
constructor() {
|
|
204
|
+
this.networkClient = new NetworkClient(DEFAULT_PAIRING_CONFIG);
|
|
205
|
+
this.storage = new PairingStorage(DEFAULT_PAIRING_CONFIG);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
disconnect(): void {
|
|
209
|
+
this.networkClient.disconnect();
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
async startTunnel(
|
|
213
|
+
deviceId?: string,
|
|
214
|
+
specificDeviceIdentifier?: string,
|
|
215
|
+
): Promise<{ socket: tls.TLSSocket; device: AppleTVDevice }> {
|
|
216
|
+
const devices = await this.discoverDevices();
|
|
217
|
+
this.logDiscoveredDevices(devices);
|
|
218
|
+
|
|
219
|
+
const devicesToProcess = this.selectDevicesToProcess(
|
|
220
|
+
devices,
|
|
221
|
+
specificDeviceIdentifier,
|
|
222
|
+
);
|
|
223
|
+
const identifiersToTry = await this.validateAndGetPairRecords(deviceId);
|
|
224
|
+
|
|
225
|
+
const failedAttempts: { identifier: string; error: string }[] = [];
|
|
226
|
+
|
|
227
|
+
for (const device of devicesToProcess) {
|
|
228
|
+
for (const identifier of identifiersToTry) {
|
|
229
|
+
appleTVLog.debug(
|
|
230
|
+
`\n--- Attempting connection with pair record: ${identifier} ---`,
|
|
231
|
+
);
|
|
232
|
+
appleTVLog.debug(`Device: ${device.ip}:${device.port}`);
|
|
233
|
+
|
|
234
|
+
const pairRecord = await this.storage.load(identifier);
|
|
235
|
+
if (!pairRecord) {
|
|
236
|
+
appleTVLog.debug(`Failed to load pair record for ${identifier}`);
|
|
237
|
+
failedAttempts.push({
|
|
238
|
+
identifier,
|
|
239
|
+
error: 'Failed to load pair record',
|
|
240
|
+
});
|
|
241
|
+
continue;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
const result = await this.attemptDeviceConnection(
|
|
245
|
+
device,
|
|
246
|
+
identifier,
|
|
247
|
+
pairRecord,
|
|
248
|
+
failedAttempts,
|
|
249
|
+
);
|
|
250
|
+
|
|
251
|
+
if (result) {
|
|
252
|
+
return result;
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
this.logFailureSummary(failedAttempts);
|
|
258
|
+
|
|
259
|
+
throw new Error(
|
|
260
|
+
'Failed to establish tunnel with any pair record. All authentication attempts failed.',
|
|
261
|
+
);
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
/**
|
|
265
|
+
* Logs information about discovered devices
|
|
266
|
+
*/
|
|
267
|
+
private logDiscoveredDevices(devices: AppleTVDevice[]): void {
|
|
268
|
+
appleTVLog.debug('Step 1: Device Discovery success');
|
|
269
|
+
appleTVLog.debug(
|
|
270
|
+
`Found ${util.pluralize('device', devices.length, true)} via Bonjour`,
|
|
271
|
+
);
|
|
272
|
+
|
|
273
|
+
if (devices.length > 0) {
|
|
274
|
+
appleTVLog.info('\nDiscovered Apple TV devices:');
|
|
275
|
+
devices.forEach((device, index) => {
|
|
276
|
+
appleTVLog.info(` ${index + 1}. Identifier: ${device.identifier}`);
|
|
277
|
+
appleTVLog.info(` Name: ${device.name}`);
|
|
278
|
+
appleTVLog.info(` IP: ${device.ip}:${device.port}`);
|
|
279
|
+
appleTVLog.info(` Model: ${device.model}`);
|
|
280
|
+
appleTVLog.info(` Version: ${device.version}`);
|
|
281
|
+
});
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
/**
|
|
286
|
+
* Selects devices to process based on the specific device identifier
|
|
287
|
+
*/
|
|
288
|
+
private selectDevicesToProcess(
|
|
289
|
+
devices: AppleTVDevice[],
|
|
290
|
+
specificDeviceIdentifier?: string,
|
|
291
|
+
): AppleTVDevice[] {
|
|
292
|
+
if (!specificDeviceIdentifier) {
|
|
293
|
+
return devices;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
const filteredDevices = devices.filter(
|
|
297
|
+
(device) => device.identifier === specificDeviceIdentifier,
|
|
298
|
+
);
|
|
299
|
+
|
|
300
|
+
if (filteredDevices.length === 0) {
|
|
301
|
+
appleTVLog.error(
|
|
302
|
+
`\nDevice with identifier ${specificDeviceIdentifier} not found in discovered devices.`,
|
|
303
|
+
);
|
|
304
|
+
appleTVLog.error('Available devices:');
|
|
305
|
+
devices.forEach((device) => {
|
|
306
|
+
appleTVLog.error(` - ${device.identifier} (${device.name})`);
|
|
307
|
+
});
|
|
308
|
+
throw new Error(
|
|
309
|
+
`Device with identifier ${specificDeviceIdentifier} not found. Please check available devices above.`,
|
|
310
|
+
);
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
appleTVLog.info(
|
|
314
|
+
`\nFiltered to specific device: ${specificDeviceIdentifier}`,
|
|
315
|
+
);
|
|
316
|
+
|
|
317
|
+
return filteredDevices;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
/**
|
|
321
|
+
* Validates pair records and returns the list of identifiers to try
|
|
322
|
+
*/
|
|
323
|
+
private async validateAndGetPairRecords(
|
|
324
|
+
deviceId?: string,
|
|
325
|
+
): Promise<string[]> {
|
|
326
|
+
const availableDeviceIds = await this.storage.getAvailableDeviceIds();
|
|
327
|
+
|
|
328
|
+
if (availableDeviceIds.length === 0) {
|
|
329
|
+
throw new Error('No pair records found');
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
if (deviceId && !availableDeviceIds.includes(deviceId)) {
|
|
333
|
+
throw new Error(`No pair record found for specified device ${deviceId}`);
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
return deviceId ? [deviceId] : availableDeviceIds;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
/**
|
|
340
|
+
* Attempts to establish a connection with a device using a specific pair record
|
|
341
|
+
*/
|
|
342
|
+
private async attemptDeviceConnection(
|
|
343
|
+
device: AppleTVDevice,
|
|
344
|
+
identifier: string,
|
|
345
|
+
pairRecord: PairRecord,
|
|
346
|
+
failedAttempts: { identifier: string; error: string }[],
|
|
347
|
+
): Promise<{ socket: tls.TLSSocket; device: AppleTVDevice } | null> {
|
|
348
|
+
try {
|
|
349
|
+
this.sequenceNumber = 0;
|
|
350
|
+
await this.networkClient.connect(device.ip!, device.port);
|
|
351
|
+
|
|
352
|
+
try {
|
|
353
|
+
await this.performHandshake();
|
|
354
|
+
|
|
355
|
+
const keys = await this.performPairVerification(pairRecord, identifier);
|
|
356
|
+
appleTVLog.info(
|
|
357
|
+
`✅ Successfully verified with pair record: ${identifier}`,
|
|
358
|
+
);
|
|
359
|
+
|
|
360
|
+
const listenerInfo = await this.createTcpListener(keys);
|
|
361
|
+
this.networkClient.disconnect();
|
|
362
|
+
|
|
363
|
+
const tlsSocket = await this.createTlsPskConnection(
|
|
364
|
+
device.ip!,
|
|
365
|
+
listenerInfo.port,
|
|
366
|
+
keys,
|
|
367
|
+
);
|
|
368
|
+
|
|
369
|
+
appleTVLog.debug('Step 6: Tunnel Establishment success');
|
|
370
|
+
appleTVLog.info(`🔑 Using pair record: ${identifier}`);
|
|
371
|
+
appleTVLog.info(
|
|
372
|
+
`📱 Connected to device: ${device.identifier} (${device.name})`,
|
|
373
|
+
);
|
|
374
|
+
appleTVLog.info(` IP: ${device.ip}:${device.port}`);
|
|
375
|
+
|
|
376
|
+
return { socket: tlsSocket, device };
|
|
377
|
+
} catch (error: any) {
|
|
378
|
+
const errorMessage = error.message || String(error);
|
|
379
|
+
appleTVLog.debug(
|
|
380
|
+
`❌ Failed with pair record ${identifier}: ${errorMessage}`,
|
|
381
|
+
);
|
|
382
|
+
failedAttempts.push({ identifier, error: errorMessage });
|
|
383
|
+
this.networkClient.disconnect();
|
|
384
|
+
await new Promise((resolve) => setTimeout(resolve, 500));
|
|
385
|
+
}
|
|
386
|
+
} catch (connectionError: any) {
|
|
387
|
+
const errorMessage = connectionError.message || String(connectionError);
|
|
388
|
+
appleTVLog.debug(
|
|
389
|
+
`Failed to connect to ${device.ip}:${device.port}: ${errorMessage}`,
|
|
390
|
+
);
|
|
391
|
+
failedAttempts.push({
|
|
392
|
+
identifier,
|
|
393
|
+
error: `Connection failed: ${errorMessage}`,
|
|
394
|
+
});
|
|
395
|
+
this.networkClient.disconnect();
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
return null;
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
/**
|
|
402
|
+
* Logs a summary of all failed connection attempts
|
|
403
|
+
*/
|
|
404
|
+
private logFailureSummary(
|
|
405
|
+
failedAttempts: { identifier: string; error: string }[],
|
|
406
|
+
): void {
|
|
407
|
+
appleTVLog.error('\n=== Pair Record Verification Summary ===');
|
|
408
|
+
appleTVLog.error(`Total pair records tried: ${failedAttempts.length}`);
|
|
409
|
+
failedAttempts.forEach(({ identifier, error }) => {
|
|
410
|
+
appleTVLog.error(` - ${identifier}: ${error}`);
|
|
411
|
+
});
|
|
412
|
+
appleTVLog.error('=======================================\n');
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
private async discoverDevices(): Promise<AppleTVDevice[]> {
|
|
416
|
+
const discovery = new BonjourDiscovery();
|
|
417
|
+
|
|
418
|
+
await discovery.startBrowsing('_remotepairing._tcp', 'local');
|
|
419
|
+
await new Promise((resolve) =>
|
|
420
|
+
setTimeout(resolve, DEFAULT_PAIRING_CONFIG.discoveryTimeout),
|
|
421
|
+
);
|
|
422
|
+
|
|
423
|
+
const services = discovery.getDiscoveredServices();
|
|
424
|
+
const devices: AppleTVDevice[] = [];
|
|
425
|
+
|
|
426
|
+
for (const service of services) {
|
|
427
|
+
try {
|
|
428
|
+
const resolved = await discovery.resolveService(
|
|
429
|
+
service.name,
|
|
430
|
+
'_remotepairing._tcp',
|
|
431
|
+
'local',
|
|
432
|
+
);
|
|
433
|
+
|
|
434
|
+
if (resolved.hostname && resolved.port) {
|
|
435
|
+
const ipResult = await lookup(
|
|
436
|
+
resolved.hostname.endsWith('.')
|
|
437
|
+
? resolved.hostname.slice(0, -1)
|
|
438
|
+
: resolved.hostname,
|
|
439
|
+
{ family: 4 },
|
|
440
|
+
);
|
|
441
|
+
|
|
442
|
+
devices.push({
|
|
443
|
+
name: resolved.name,
|
|
444
|
+
identifier: resolved.txtRecord?.identifier || resolved.name,
|
|
445
|
+
hostname: resolved.hostname,
|
|
446
|
+
ip: ipResult.address,
|
|
447
|
+
port: resolved.port,
|
|
448
|
+
model: resolved.txtRecord?.model || '',
|
|
449
|
+
version: resolved.txtRecord?.ver || '',
|
|
450
|
+
minVersion: resolved.txtRecord?.minVer || '17',
|
|
451
|
+
});
|
|
452
|
+
}
|
|
453
|
+
} catch (err) {
|
|
454
|
+
appleTVLog.debug(`Failed to resolve service ${service.name}: ${err}`);
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
discovery.stopBrowsing();
|
|
459
|
+
|
|
460
|
+
if (devices.length === 0) {
|
|
461
|
+
throw new Error('No devices found via Bonjour discovery');
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
return devices;
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
private async performHandshake(): Promise<void> {
|
|
468
|
+
const handshakePayload = {
|
|
469
|
+
message: {
|
|
470
|
+
plain: {
|
|
471
|
+
_0: {
|
|
472
|
+
request: {
|
|
473
|
+
_0: {
|
|
474
|
+
handshake: {
|
|
475
|
+
_0: {
|
|
476
|
+
hostOptions: { attemptPairVerify: true },
|
|
477
|
+
wireProtocolVersion: 19,
|
|
478
|
+
},
|
|
479
|
+
},
|
|
480
|
+
},
|
|
481
|
+
},
|
|
482
|
+
},
|
|
483
|
+
},
|
|
484
|
+
},
|
|
485
|
+
originatedBy: 'host',
|
|
486
|
+
sequenceNumber: this.sequenceNumber++,
|
|
487
|
+
};
|
|
488
|
+
|
|
489
|
+
await this.networkClient.sendPacket(handshakePayload);
|
|
490
|
+
await this.networkClient.receiveResponse();
|
|
491
|
+
|
|
492
|
+
appleTVLog.debug('Step 2: Initial Connection & Handshake success');
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
private async performPairVerification(
|
|
496
|
+
pairRecord: PairRecord,
|
|
497
|
+
deviceId: string,
|
|
498
|
+
): Promise<VerificationKeys> {
|
|
499
|
+
appleTVLog.debug('Step 3: Pair Verification (4-step process)');
|
|
500
|
+
|
|
501
|
+
const verificationProtocol = new PairVerificationProtocol(
|
|
502
|
+
this.networkClient,
|
|
503
|
+
);
|
|
504
|
+
verificationProtocol.setSequenceNumber(this.sequenceNumber);
|
|
505
|
+
const keys = await verificationProtocol.verify(pairRecord, deviceId);
|
|
506
|
+
|
|
507
|
+
this.sequenceNumber = verificationProtocol.getSequenceNumber();
|
|
508
|
+
|
|
509
|
+
appleTVLog.debug('Step 4: Main Encryption Key Derivation success');
|
|
510
|
+
|
|
511
|
+
return keys;
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
private async createTcpListener(
|
|
515
|
+
keys: VerificationKeys,
|
|
516
|
+
): Promise<{ port: number; serviceName: string; devicePublicKey: string }> {
|
|
517
|
+
appleTVLog.debug('Step 5: TCP Listener Creation (Encrypted Request)');
|
|
518
|
+
|
|
519
|
+
const tunnelService = new TunnelService(
|
|
520
|
+
this.networkClient,
|
|
521
|
+
keys,
|
|
522
|
+
this.sequenceNumber,
|
|
523
|
+
);
|
|
524
|
+
const listenerInfo = await tunnelService.createTcpListener();
|
|
525
|
+
|
|
526
|
+
this.sequenceNumber = tunnelService.getSequenceNumber();
|
|
527
|
+
|
|
528
|
+
return listenerInfo;
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
private async createTlsPskConnection(
|
|
532
|
+
hostname: string,
|
|
533
|
+
port: number,
|
|
534
|
+
keys: VerificationKeys,
|
|
535
|
+
): Promise<tls.TLSSocket> {
|
|
536
|
+
const tunnelService = new TunnelService(
|
|
537
|
+
this.networkClient,
|
|
538
|
+
keys,
|
|
539
|
+
this.sequenceNumber,
|
|
540
|
+
);
|
|
541
|
+
return tunnelService.createTlsPskConnection(hostname, port);
|
|
542
|
+
}
|
|
543
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type * as tls from 'node:tls';
|
|
2
|
+
|
|
3
|
+
export interface TcpListenerInfo {
|
|
4
|
+
port: number;
|
|
5
|
+
serviceName: string;
|
|
6
|
+
devicePublicKey: string;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Extended TLS connection options that include PSK (Pre-Shared Key) callback support.
|
|
11
|
+
* This interface extends the standard Node.js TLS ConnectionOptions to add PSK authentication.
|
|
12
|
+
*/
|
|
13
|
+
export interface TlsPskConnectionOptions extends tls.ConnectionOptions {
|
|
14
|
+
/**
|
|
15
|
+
* Callback function invoked during TLS handshake to provide PSK credentials.
|
|
16
|
+
* @param hint - Optional hint from the server about which PSK identity to use
|
|
17
|
+
* @returns Object containing the pre-shared key and identity string
|
|
18
|
+
*/
|
|
19
|
+
pskCallback?: (hint: string | null) => {
|
|
20
|
+
psk: Buffer;
|
|
21
|
+
identity: string;
|
|
22
|
+
};
|
|
23
|
+
}
|
|
@@ -446,7 +446,7 @@ export class BonjourDiscovery extends EventEmitter {
|
|
|
446
446
|
return;
|
|
447
447
|
}
|
|
448
448
|
|
|
449
|
-
log.info(
|
|
449
|
+
log.info('Starting Bonjour discovery');
|
|
450
450
|
|
|
451
451
|
try {
|
|
452
452
|
await this.initializeBrowsing(serviceType, domain);
|
|
@@ -482,9 +482,7 @@ export class BonjourDiscovery extends EventEmitter {
|
|
|
482
482
|
serviceType: string = BONJOUR_SERVICE_TYPES.APPLE_TV_PAIRING,
|
|
483
483
|
domain: string = BONJOUR_DEFAULT_DOMAIN,
|
|
484
484
|
): Promise<BonjourService> {
|
|
485
|
-
log.info(
|
|
486
|
-
`[ServiceResolver] Resolving service: ${serviceName}.${serviceType}.${domain}`,
|
|
487
|
-
);
|
|
485
|
+
log.info(`[ServiceResolver] Resolving service: ${serviceName}`);
|
|
488
486
|
|
|
489
487
|
const service = await executeDnsSdCommand<BonjourService | null>(
|
|
490
488
|
[DNS_SD_COMMANDS.RESOLVE, serviceName, serviceType, domain],
|
|
@@ -538,7 +536,7 @@ export class BonjourDiscovery extends EventEmitter {
|
|
|
538
536
|
case DNS_SD_ACTIONS.ADD:
|
|
539
537
|
this._discoveredServices.set(service.name, service);
|
|
540
538
|
this.emit('serviceAdded', service);
|
|
541
|
-
log.
|
|
539
|
+
log.debug(`Discovered service: ${service.name}`);
|
|
542
540
|
break;
|
|
543
541
|
case DNS_SD_ACTIONS.REMOVE:
|
|
544
542
|
this._discoveredServices.delete(service.name);
|
|
@@ -43,13 +43,6 @@ interface SessionInfo {
|
|
|
43
43
|
enableSessionSSL: boolean;
|
|
44
44
|
}
|
|
45
45
|
|
|
46
|
-
interface StartSessionRequest {
|
|
47
|
-
Label: string;
|
|
48
|
-
Request: string;
|
|
49
|
-
HostID: string;
|
|
50
|
-
SystemBUID: string;
|
|
51
|
-
}
|
|
52
|
-
|
|
53
46
|
interface StartSessionResponse {
|
|
54
47
|
Request?: string;
|
|
55
48
|
SessionID?: PlistValue;
|
|
@@ -93,13 +93,6 @@ export class LengthBasedSplitter extends Transform {
|
|
|
93
93
|
// Add the new chunk to our buffer
|
|
94
94
|
this.buffer = Buffer.concat([this.buffer, chunk]);
|
|
95
95
|
|
|
96
|
-
// Check if this is XML data or binary plist before doing any other processing
|
|
97
|
-
const bufferString = this.buffer.toString(
|
|
98
|
-
UTF8_ENCODING,
|
|
99
|
-
0,
|
|
100
|
-
Math.min(MAX_PREVIEW_LENGTH, this.buffer.length),
|
|
101
|
-
);
|
|
102
|
-
|
|
103
96
|
// Check for binary plist format (bplist00 or Ibplist00)
|
|
104
97
|
if (this.buffer.length >= BINARY_PLIST_HEADER_LENGTH) {
|
|
105
98
|
const possibleBplistHeader = this.buffer.toString(
|
|
@@ -236,26 +229,11 @@ export class LengthBasedSplitter extends Transform {
|
|
|
236
229
|
if (alternateLength > 0 && alternateLength <= this.maxFrameLength) {
|
|
237
230
|
messageLength = alternateLength;
|
|
238
231
|
} else {
|
|
239
|
-
// If length is still invalid, check if this might actually be XML
|
|
240
|
-
const suspiciousData = this.buffer.toString(
|
|
241
|
-
UTF8_ENCODING,
|
|
242
|
-
0,
|
|
243
|
-
Math.min(MAX_PREVIEW_LENGTH, this.buffer.length),
|
|
244
|
-
);
|
|
245
|
-
|
|
246
232
|
// Invalid length - skip one byte and try again
|
|
247
233
|
this.buffer = this.buffer.slice(1);
|
|
248
234
|
continue;
|
|
249
235
|
}
|
|
250
236
|
} else {
|
|
251
|
-
// For non-4-byte length fields, just use the original approach
|
|
252
|
-
// If length is invalid, check if this might actually be XML
|
|
253
|
-
const suspiciousData = this.buffer.toString(
|
|
254
|
-
UTF8_ENCODING,
|
|
255
|
-
0,
|
|
256
|
-
Math.min(MAX_PREVIEW_LENGTH, this.buffer.length),
|
|
257
|
-
);
|
|
258
|
-
|
|
259
237
|
// Invalid length - skip one byte and try again
|
|
260
238
|
this.buffer = this.buffer.slice(1);
|
|
261
239
|
continue;
|
package/src/lib/tunnel/index.ts
CHANGED
|
@@ -19,14 +19,6 @@ interface TunnelRegistryEntry {
|
|
|
19
19
|
remoteXPC?: RemoteXpcConnection;
|
|
20
20
|
}
|
|
21
21
|
|
|
22
|
-
/**
|
|
23
|
-
* Interface for tunnel and RemoteXPC connection result
|
|
24
|
-
*/
|
|
25
|
-
interface TunnelResult {
|
|
26
|
-
tunnel: TunnelConnection;
|
|
27
|
-
remoteXPC: RemoteXpcConnection;
|
|
28
|
-
}
|
|
29
|
-
|
|
30
22
|
/**
|
|
31
23
|
* A wrapper around the tunnel connection that
|
|
32
24
|
* maintains a registry of active tunnels that can be reused.
|
|
@@ -256,3 +248,5 @@ export const TunnelManager = new TunnelManagerService();
|
|
|
256
248
|
// Export packet streaming IPC functionality
|
|
257
249
|
export { PacketStreamClient } from './packet-stream-client.js';
|
|
258
250
|
export { PacketStreamServer } from './packet-stream-server.js';
|
|
251
|
+
// Re-export TunnelConnection type from appium-ios-tuntap
|
|
252
|
+
export type { TunnelConnection };
|
|
@@ -6,14 +6,6 @@ import { getLogger } from '../logger.js';
|
|
|
6
6
|
|
|
7
7
|
const log = getLogger('PacketStreamServer');
|
|
8
8
|
|
|
9
|
-
/**
|
|
10
|
-
* Interface for serialized packet message
|
|
11
|
-
*/
|
|
12
|
-
interface SerializedPacketMessage {
|
|
13
|
-
length: string;
|
|
14
|
-
data: string;
|
|
15
|
-
}
|
|
16
|
-
|
|
17
9
|
/**
|
|
18
10
|
* Server that exposes packet streaming from a tunnel over TCP
|
|
19
11
|
* This allows cross-process access to tunnel packet streams
|
|
@@ -5,7 +5,7 @@ import { getLogger } from '../logger.js';
|
|
|
5
5
|
import type { TunnelRegistry, TunnelRegistryEntry } from '../types.js';
|
|
6
6
|
|
|
7
7
|
// Constants
|
|
8
|
-
const DEFAULT_TUNNEL_REGISTRY_PORT = 42314;
|
|
8
|
+
export const DEFAULT_TUNNEL_REGISTRY_PORT = 42314;
|
|
9
9
|
const API_BASE_PATH = '/remotexpc/tunnels';
|
|
10
10
|
|
|
11
11
|
// Logger instance
|