appium-ios-remotexpc 0.21.2 → 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 +6 -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 +1 -1
- 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 +0 -2
- package/src/services/ios/afc/stream-utils.ts +0 -2
- package/src/services/ios/mobile-config/index.ts +2 -2
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
#!/usr/bin/env tsx
|
|
2
|
+
import * as tls from 'node:tls';
|
|
3
|
+
|
|
4
|
+
import { PacketStreamServer, TunnelManager } from '../src/index.js';
|
|
5
|
+
import type { TunnelRegistry } from '../src/index.js';
|
|
6
|
+
import { AppleTVTunnelService } from '../src/lib/apple-tv/tunnel/index.js';
|
|
7
|
+
import type { AppleTVDevice } from '../src/lib/bonjour/bonjour-discovery.js';
|
|
8
|
+
import { getLogger } from '../src/lib/logger.js';
|
|
9
|
+
import type { TunnelConnection } from '../src/lib/tunnel/index.js';
|
|
10
|
+
import {
|
|
11
|
+
DEFAULT_TUNNEL_REGISTRY_PORT,
|
|
12
|
+
startTunnelRegistryServer,
|
|
13
|
+
} from '../src/lib/tunnel/tunnel-registry-server.js';
|
|
14
|
+
|
|
15
|
+
const log = getLogger('WiFiTunnel');
|
|
16
|
+
const PACKET_STREAM_PORT = 50100;
|
|
17
|
+
|
|
18
|
+
async function main(): Promise<void> {
|
|
19
|
+
const args = process.argv.slice(2);
|
|
20
|
+
const specificDeviceIdentifier = args.find((arg) => !arg.startsWith('-'));
|
|
21
|
+
|
|
22
|
+
if (specificDeviceIdentifier) {
|
|
23
|
+
log.info(
|
|
24
|
+
`Starting Apple TV tunnel for specific device identifier: ${specificDeviceIdentifier}`,
|
|
25
|
+
);
|
|
26
|
+
} else {
|
|
27
|
+
log.info('Starting Apple TV tunnel (will try all discovered devices)');
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const tunnelService = new AppleTVTunnelService();
|
|
31
|
+
let tunnel: TunnelConnection | null = null;
|
|
32
|
+
let tlsSocket: tls.TLSSocket | null = null;
|
|
33
|
+
let deviceInfo: AppleTVDevice | null = null;
|
|
34
|
+
let packetStreamServer: PacketStreamServer | null = null;
|
|
35
|
+
|
|
36
|
+
const cleanup = async (signal: string): Promise<void> => {
|
|
37
|
+
log.warn(`\nCleaning up (${signal})...`);
|
|
38
|
+
|
|
39
|
+
try {
|
|
40
|
+
// Close packet stream server first
|
|
41
|
+
if (packetStreamServer) {
|
|
42
|
+
log.info('Closing packet stream server...');
|
|
43
|
+
await packetStreamServer.stop();
|
|
44
|
+
packetStreamServer = null;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
if (tunnel && typeof tunnel.closer === 'function') {
|
|
48
|
+
log.info('Closing tunnel...');
|
|
49
|
+
await tunnel.closer();
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
if (tlsSocket && !tlsSocket.destroyed) {
|
|
53
|
+
log.info('Closing TLS-PSK connection...');
|
|
54
|
+
tlsSocket.destroy();
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
tunnelService.disconnect();
|
|
58
|
+
|
|
59
|
+
log.info('Cleanup completed.');
|
|
60
|
+
} catch (err) {
|
|
61
|
+
log.error('Error during cleanup:', err);
|
|
62
|
+
}
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
process.on('SIGINT', async () => {
|
|
66
|
+
await cleanup('SIGINT (Ctrl+C)');
|
|
67
|
+
process.exit(0);
|
|
68
|
+
});
|
|
69
|
+
process.on('SIGTERM', async () => {
|
|
70
|
+
await cleanup('SIGTERM');
|
|
71
|
+
process.exit(0);
|
|
72
|
+
});
|
|
73
|
+
process.on('SIGHUP', async () => {
|
|
74
|
+
await cleanup('SIGHUP');
|
|
75
|
+
process.exit(0);
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
process.on('uncaughtException', async (error) => {
|
|
79
|
+
log.error('Uncaught Exception:', error);
|
|
80
|
+
await cleanup('Uncaught Exception');
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
process.on('unhandledRejection', async (reason, promise) => {
|
|
84
|
+
log.error('Unhandled Rejection at:', promise, 'reason:', reason);
|
|
85
|
+
await cleanup('Unhandled Rejection');
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
try {
|
|
89
|
+
log.info('Starting Apple TV tunnel...');
|
|
90
|
+
const result = await tunnelService.startTunnel(
|
|
91
|
+
undefined,
|
|
92
|
+
specificDeviceIdentifier,
|
|
93
|
+
);
|
|
94
|
+
tlsSocket = result.socket;
|
|
95
|
+
deviceInfo = result.device;
|
|
96
|
+
|
|
97
|
+
if (!tlsSocket) {
|
|
98
|
+
throw new Error('TLS-PSK socket not established');
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
log.info('Creating tunnel with TunnelManager...');
|
|
102
|
+
tunnel = await TunnelManager.getTunnel(tlsSocket);
|
|
103
|
+
|
|
104
|
+
// Start packet stream server (same as iPhone tunnel)
|
|
105
|
+
let packetStreamPort = 0;
|
|
106
|
+
try {
|
|
107
|
+
packetStreamServer = new PacketStreamServer(PACKET_STREAM_PORT);
|
|
108
|
+
await packetStreamServer.start();
|
|
109
|
+
|
|
110
|
+
// Attach packet consumer to tunnel to receive packet data
|
|
111
|
+
const consumer = packetStreamServer.getPacketConsumer();
|
|
112
|
+
if (consumer && tunnel.addPacketConsumer) {
|
|
113
|
+
tunnel.addPacketConsumer(consumer);
|
|
114
|
+
log.info(`Packet stream server started on port ${PACKET_STREAM_PORT}`);
|
|
115
|
+
packetStreamPort = PACKET_STREAM_PORT;
|
|
116
|
+
} else {
|
|
117
|
+
log.warn('Failed to attach packet consumer to tunnel');
|
|
118
|
+
}
|
|
119
|
+
} catch (err) {
|
|
120
|
+
log.warn(`Failed to start packet stream server: ${err}`);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const now = Date.now();
|
|
124
|
+
const nowISOString = new Date().toISOString();
|
|
125
|
+
|
|
126
|
+
const registry: TunnelRegistry = {
|
|
127
|
+
tunnels: {
|
|
128
|
+
[deviceInfo.identifier]: {
|
|
129
|
+
udid: deviceInfo.identifier,
|
|
130
|
+
deviceId: 0,
|
|
131
|
+
address: tunnel.Address,
|
|
132
|
+
rsdPort: tunnel.RsdPort ?? 0,
|
|
133
|
+
packetStreamPort,
|
|
134
|
+
connectionType: 'WiFi',
|
|
135
|
+
productId: 0,
|
|
136
|
+
createdAt: now,
|
|
137
|
+
lastUpdated: now,
|
|
138
|
+
},
|
|
139
|
+
},
|
|
140
|
+
metadata: {
|
|
141
|
+
lastUpdated: nowISOString,
|
|
142
|
+
totalTunnels: 1,
|
|
143
|
+
activeTunnels: 1,
|
|
144
|
+
},
|
|
145
|
+
};
|
|
146
|
+
|
|
147
|
+
await startTunnelRegistryServer(registry);
|
|
148
|
+
|
|
149
|
+
log.info('=== TUNNEL ESTABLISHED ===');
|
|
150
|
+
log.info(`Tunnel Address: ${tunnel.Address}`);
|
|
151
|
+
log.info(`Tunnel RSD Port: ${tunnel.RsdPort}`);
|
|
152
|
+
log.info(`Packet Stream Port: ${packetStreamPort}`);
|
|
153
|
+
log.info('==========================');
|
|
154
|
+
|
|
155
|
+
log.info('\n📁 Tunnel registry API:');
|
|
156
|
+
log.info(
|
|
157
|
+
` http://localhost:${DEFAULT_TUNNEL_REGISTRY_PORT}/remotexpc/tunnels`,
|
|
158
|
+
);
|
|
159
|
+
log.info(' - GET /remotexpc/tunnels - List all tunnels');
|
|
160
|
+
log.info(
|
|
161
|
+
` - GET /remotexpc/tunnels/${deviceInfo.identifier} - Get tunnel by identifier`,
|
|
162
|
+
);
|
|
163
|
+
log.info(' - GET /remotexpc/tunnels/metadata - Get registry metadata');
|
|
164
|
+
|
|
165
|
+
log.info('\n✅ Apple TV tunnel is ready!');
|
|
166
|
+
log.info(`Use: --rsd ${tunnel.Address} ${tunnel.RsdPort}`);
|
|
167
|
+
log.info('\nPress Ctrl+C to close the tunnel and exit.');
|
|
168
|
+
|
|
169
|
+
process.stdin.resume();
|
|
170
|
+
} catch (error) {
|
|
171
|
+
log.error('Tunnel failed:', error);
|
|
172
|
+
throw error;
|
|
173
|
+
} finally {
|
|
174
|
+
await cleanup('Shutdown');
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
main();
|
|
@@ -8,14 +8,16 @@ import type { ConnectionOptions } from 'tls';
|
|
|
8
8
|
|
|
9
9
|
import {
|
|
10
10
|
PacketStreamServer,
|
|
11
|
-
SocketInfo,
|
|
12
11
|
TunnelManager,
|
|
13
12
|
createLockdownServiceByUDID,
|
|
14
13
|
createUsbmux,
|
|
15
14
|
startCoreDeviceProxy,
|
|
16
|
-
TunnelRegistry,
|
|
17
15
|
} from '../src/index.js';
|
|
18
|
-
import {
|
|
16
|
+
import type { SocketInfo, TunnelRegistry } from '../src/index.js';
|
|
17
|
+
import {
|
|
18
|
+
DEFAULT_TUNNEL_REGISTRY_PORT,
|
|
19
|
+
startTunnelRegistryServer,
|
|
20
|
+
} from '../src/lib/tunnel/tunnel-registry-server.js';
|
|
19
21
|
import type { Device } from '../src/lib/usbmux/index.js';
|
|
20
22
|
|
|
21
23
|
const log = logger.getLogger('TunnelCreation');
|
|
@@ -27,7 +29,7 @@ async function updateTunnelRegistry(
|
|
|
27
29
|
): Promise<TunnelRegistry> {
|
|
28
30
|
const now = Date.now();
|
|
29
31
|
const nowISOString = new Date().toISOString();
|
|
30
|
-
|
|
32
|
+
|
|
31
33
|
// Initialize registry if it doesn't exist
|
|
32
34
|
const registry: TunnelRegistry = {
|
|
33
35
|
tunnels: {},
|
|
@@ -35,7 +37,7 @@ async function updateTunnelRegistry(
|
|
|
35
37
|
lastUpdated: nowISOString,
|
|
36
38
|
totalTunnels: 0,
|
|
37
39
|
activeTunnels: 0,
|
|
38
|
-
}
|
|
40
|
+
},
|
|
39
41
|
};
|
|
40
42
|
|
|
41
43
|
// Update tunnels
|
|
@@ -47,7 +49,7 @@ async function updateTunnelRegistry(
|
|
|
47
49
|
deviceId: result.device.DeviceID,
|
|
48
50
|
address: result.tunnel.Address,
|
|
49
51
|
rsdPort: result.tunnel.RsdPort ?? 0,
|
|
50
|
-
packetStreamPort: result.packetStreamPort,
|
|
52
|
+
packetStreamPort: result.packetStreamPort ?? 0,
|
|
51
53
|
connectionType: result.device.Properties.ConnectionType,
|
|
52
54
|
productId: result.device.Properties.ProductID,
|
|
53
55
|
createdAt: registry.tunnels[udid]?.createdAt ?? now,
|
|
@@ -86,7 +88,7 @@ let PACKET_STREAM_BASE_PORT = 50000;
|
|
|
86
88
|
*/
|
|
87
89
|
function setupCleanupHandlers(): void {
|
|
88
90
|
const cleanup = async (signal: string) => {
|
|
89
|
-
log.warn(`\
|
|
91
|
+
log.warn(`\nCleaning up (${signal})...`);
|
|
90
92
|
|
|
91
93
|
// Close all packet stream servers
|
|
92
94
|
if (packetStreamServers.size > 0) {
|
|
@@ -106,26 +108,32 @@ function setupCleanupHandlers(): void {
|
|
|
106
108
|
packetStreamServers.clear();
|
|
107
109
|
}
|
|
108
110
|
|
|
109
|
-
log.info('Cleanup completed.
|
|
110
|
-
process.exit(0);
|
|
111
|
+
log.info('Cleanup completed.');
|
|
111
112
|
};
|
|
112
113
|
|
|
113
114
|
// Handle various termination signals
|
|
114
|
-
process.on('SIGINT', () =>
|
|
115
|
-
|
|
116
|
-
|
|
115
|
+
process.on('SIGINT', async () => {
|
|
116
|
+
await cleanup('SIGINT (Ctrl+C)');
|
|
117
|
+
process.exit(0);
|
|
118
|
+
});
|
|
119
|
+
process.on('SIGTERM', async () => {
|
|
120
|
+
await cleanup('SIGTERM');
|
|
121
|
+
process.exit(0);
|
|
122
|
+
});
|
|
123
|
+
process.on('SIGHUP', async () => {
|
|
124
|
+
await cleanup('SIGHUP');
|
|
125
|
+
process.exit(0);
|
|
126
|
+
});
|
|
117
127
|
|
|
118
128
|
// Handle uncaught exceptions and unhandled rejections
|
|
119
129
|
process.on('uncaughtException', async (error) => {
|
|
120
130
|
log.error('Uncaught Exception:', error);
|
|
121
131
|
await cleanup('Uncaught Exception');
|
|
122
|
-
process.exit(1);
|
|
123
132
|
});
|
|
124
133
|
|
|
125
134
|
process.on('unhandledRejection', async (reason, promise) => {
|
|
126
135
|
log.error('Unhandled Rejection at:', promise, 'reason:', reason);
|
|
127
136
|
await cleanup('Unhandled Rejection');
|
|
128
|
-
process.exit(1);
|
|
129
137
|
});
|
|
130
138
|
}
|
|
131
139
|
|
|
@@ -225,7 +233,7 @@ async function createTunnelForDevice(
|
|
|
225
233
|
},
|
|
226
234
|
packetStreamPort,
|
|
227
235
|
success: true,
|
|
228
|
-
socket
|
|
236
|
+
socket,
|
|
229
237
|
};
|
|
230
238
|
} catch (err) {
|
|
231
239
|
log.warn(`Could not add device to info server: ${err}`);
|
|
@@ -356,23 +364,24 @@ async function main(): Promise<void> {
|
|
|
356
364
|
log.info(' - GET /remotexpc/tunnels/metadata - Get registry metadata');
|
|
357
365
|
|
|
358
366
|
log.info('\n💡 Example usage:');
|
|
359
|
-
log.info(
|
|
360
|
-
|
|
367
|
+
log.info(
|
|
368
|
+
` curl http://localhost:${DEFAULT_TUNNEL_REGISTRY_PORT}/remotexpc/tunnels`,
|
|
369
|
+
);
|
|
370
|
+
log.info(
|
|
371
|
+
` curl http://localhost:${DEFAULT_TUNNEL_REGISTRY_PORT}/remotexpc/tunnels/metadata`,
|
|
372
|
+
);
|
|
361
373
|
if (successful.length > 0) {
|
|
362
374
|
const firstUdid = successful[0].device.Properties.SerialNumber;
|
|
363
375
|
log.info(
|
|
364
|
-
` curl http://localhost
|
|
376
|
+
` curl http://localhost:${DEFAULT_TUNNEL_REGISTRY_PORT}/remotexpc/tunnels/${firstUdid}`,
|
|
365
377
|
);
|
|
366
378
|
}
|
|
367
379
|
}
|
|
368
380
|
} catch (error) {
|
|
369
381
|
log.error(`Error during tunnel creation test: ${error}`);
|
|
370
|
-
|
|
382
|
+
throw error;
|
|
371
383
|
}
|
|
372
384
|
}
|
|
373
385
|
|
|
374
386
|
// Run the main function
|
|
375
|
-
main()
|
|
376
|
-
log.error(`Fatal error: ${error}`);
|
|
377
|
-
process.exit(1);
|
|
378
|
-
});
|
|
387
|
+
main();
|
|
@@ -1,10 +1,18 @@
|
|
|
1
1
|
// Default configuration values for Apple TV pairing behavior
|
|
2
|
+
// These can be overridden via environment variables:
|
|
3
|
+
// - APPLETV_TIMEOUT: Network operation timeout in milliseconds (default: 30000)
|
|
4
|
+
// - APPLETV_DISCOVERY_TIMEOUT: Device discovery timeout in milliseconds (default: 10000)
|
|
5
|
+
// 10 seconds provides reliable Apple TV discovery via mDNS/Bonjour across various network conditions
|
|
6
|
+
// - APPLETV_MAX_RETRIES: Maximum retry attempts (default: 3)
|
|
2
7
|
export const DEFAULT_PAIRING_CONFIG = {
|
|
3
|
-
timeout: 30000,
|
|
4
|
-
discoveryTimeout:
|
|
5
|
-
maxRetries: 3,
|
|
8
|
+
timeout: Number(process.env.APPLETV_TIMEOUT) || 30000,
|
|
9
|
+
discoveryTimeout: Number(process.env.APPLETV_DISCOVERY_TIMEOUT) || 10000,
|
|
10
|
+
maxRetries: Number(process.env.APPLETV_MAX_RETRIES) || 3,
|
|
6
11
|
} as const;
|
|
7
12
|
|
|
13
|
+
// Prefix used for Apple TV pairing record storage identifiers
|
|
14
|
+
export const APPLETV_PAIRING_PREFIX = 'appletv_pairing_';
|
|
15
|
+
|
|
8
16
|
// TLV8 component type identifiers used in pairing data exchange
|
|
9
17
|
export const PairingDataComponentType = {
|
|
10
18
|
METHOD: 0x00,
|
|
@@ -1,15 +1,14 @@
|
|
|
1
|
-
import { logger } from '@appium/support';
|
|
2
|
-
|
|
3
1
|
import {
|
|
4
2
|
type AppleTVDevice,
|
|
5
3
|
BonjourDiscovery,
|
|
6
4
|
} from '../../bonjour/bonjour-discovery.js';
|
|
5
|
+
import { getLogger } from '../../logger.js';
|
|
7
6
|
import { PairingError } from '../errors.js';
|
|
8
7
|
import type { PairingConfig } from '../types.js';
|
|
9
8
|
|
|
10
9
|
/** Discovers Apple TV devices on the local network using Bonjour */
|
|
11
10
|
export class DeviceDiscoveryService {
|
|
12
|
-
private readonly log =
|
|
11
|
+
private readonly log = getLogger('DeviceDiscoveryService');
|
|
13
12
|
|
|
14
13
|
constructor(private readonly config: PairingConfig) {}
|
|
15
14
|
|
|
@@ -8,4 +8,10 @@ export {
|
|
|
8
8
|
|
|
9
9
|
export { generateEd25519KeyPair, createEd25519Signature } from './ed25519.js';
|
|
10
10
|
|
|
11
|
+
export {
|
|
12
|
+
generateX25519KeyPair,
|
|
13
|
+
performX25519DiffieHellman,
|
|
14
|
+
type X25519KeyPair,
|
|
15
|
+
} from './x25519.js';
|
|
16
|
+
|
|
11
17
|
export { hkdf, type HKDFParams } from './hkdf.js';
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import {
|
|
2
|
+
type KeyObject,
|
|
3
|
+
createPublicKey,
|
|
4
|
+
diffieHellman,
|
|
5
|
+
generateKeyPairSync,
|
|
6
|
+
} from 'node:crypto';
|
|
7
|
+
|
|
8
|
+
import { getLogger } from '../../logger.js';
|
|
9
|
+
import { CryptographyError } from '../errors.js';
|
|
10
|
+
|
|
11
|
+
const log = getLogger('X25519');
|
|
12
|
+
|
|
13
|
+
const X25519_PUBLIC_KEY_LENGTH = 32;
|
|
14
|
+
const X25519_SPKI_PREFIX = Buffer.from([
|
|
15
|
+
0x30, 0x2a, 0x30, 0x05, 0x06, 0x03, 0x2b, 0x65, 0x6e, 0x03, 0x21, 0x00,
|
|
16
|
+
]);
|
|
17
|
+
|
|
18
|
+
export interface X25519KeyPair {
|
|
19
|
+
publicKey: Buffer;
|
|
20
|
+
privateKey: KeyObject;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function generateX25519KeyPair(): X25519KeyPair {
|
|
24
|
+
try {
|
|
25
|
+
const { publicKey, privateKey } = generateKeyPairSync('x25519');
|
|
26
|
+
|
|
27
|
+
const publicKeyDer = publicKey.export({
|
|
28
|
+
type: 'spki',
|
|
29
|
+
format: 'der',
|
|
30
|
+
}) as Buffer;
|
|
31
|
+
const publicKeyBytes = publicKeyDer.subarray(-X25519_PUBLIC_KEY_LENGTH);
|
|
32
|
+
|
|
33
|
+
return {
|
|
34
|
+
publicKey: publicKeyBytes,
|
|
35
|
+
privateKey,
|
|
36
|
+
};
|
|
37
|
+
} catch (error) {
|
|
38
|
+
log.error('Failed to generate X25519 key pair:', error);
|
|
39
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
40
|
+
throw new CryptographyError(
|
|
41
|
+
`Failed to generate X25519 key pair: ${message}`,
|
|
42
|
+
);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function performX25519DiffieHellman(
|
|
47
|
+
privateKey: KeyObject,
|
|
48
|
+
devicePublicKey: Buffer,
|
|
49
|
+
): Buffer {
|
|
50
|
+
if (!devicePublicKey || devicePublicKey.length !== X25519_PUBLIC_KEY_LENGTH) {
|
|
51
|
+
throw new CryptographyError(
|
|
52
|
+
`Device public key must be ${X25519_PUBLIC_KEY_LENGTH} bytes`,
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
try {
|
|
57
|
+
const devicePublicKeySpki = Buffer.concat([
|
|
58
|
+
X25519_SPKI_PREFIX,
|
|
59
|
+
devicePublicKey,
|
|
60
|
+
]);
|
|
61
|
+
|
|
62
|
+
const publicKeyObject = createPublicKey({
|
|
63
|
+
key: devicePublicKeySpki,
|
|
64
|
+
format: 'der',
|
|
65
|
+
type: 'spki',
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
return diffieHellman({
|
|
69
|
+
privateKey,
|
|
70
|
+
publicKey: publicKeyObject,
|
|
71
|
+
});
|
|
72
|
+
} catch (error) {
|
|
73
|
+
log.error('Failed to perform X25519 Diffie-Hellman:', error);
|
|
74
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
75
|
+
throw new CryptographyError(
|
|
76
|
+
`Failed to perform X25519 Diffie-Hellman: ${message}`,
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
@@ -20,5 +20,6 @@ export * from './srp/index.js';
|
|
|
20
20
|
export * from './network/index.js';
|
|
21
21
|
export * from './pairing-protocol/index.js';
|
|
22
22
|
export * from './storage/index.js';
|
|
23
|
+
export * from './tunnel/index.js';
|
|
23
24
|
export * from './discovery/index.js';
|
|
24
25
|
export * from './pairing/index.js';
|
|
@@ -1,12 +1,12 @@
|
|
|
1
|
-
import { logger } from '@appium/support';
|
|
2
1
|
import * as net from 'node:net';
|
|
3
2
|
|
|
3
|
+
import { getLogger } from '../../logger.js';
|
|
4
4
|
import { NetworkError } from '../errors.js';
|
|
5
5
|
import type { PairingConfig } from '../types.js';
|
|
6
6
|
import { NETWORK_CONSTANTS } from './constants.js';
|
|
7
7
|
import type { NetworkClientInterface } from './types.js';
|
|
8
8
|
|
|
9
|
-
const log =
|
|
9
|
+
const log = getLogger('NetworkClient');
|
|
10
10
|
|
|
11
11
|
/** Handles TCP socket communication with Apple TV devices */
|
|
12
12
|
export class NetworkClient implements NetworkClientInterface {
|
|
@@ -1,13 +1,13 @@
|
|
|
1
|
-
import { logger } from '@appium/support';
|
|
2
1
|
import { createInterface } from 'node:readline';
|
|
3
2
|
|
|
3
|
+
import { getLogger } from '../../logger.js';
|
|
4
4
|
import { PairingError } from '../errors.js';
|
|
5
5
|
import { NETWORK_CONSTANTS } from '../network/constants.js';
|
|
6
6
|
import type { UserInputInterface } from '../pairing-protocol/types.js';
|
|
7
7
|
|
|
8
8
|
/** Handles user interaction for PIN input during pairing */
|
|
9
9
|
export class UserInputService implements UserInputInterface {
|
|
10
|
-
private readonly log =
|
|
10
|
+
private readonly log = getLogger('UserInputService');
|
|
11
11
|
|
|
12
12
|
async promptForPIN(): Promise<string> {
|
|
13
13
|
const rl = createInterface({
|
|
@@ -15,5 +15,34 @@ export const PAIRING_MESSAGES = {
|
|
|
15
15
|
M6_NONCE: 'PS-Msg06',
|
|
16
16
|
} as const;
|
|
17
17
|
|
|
18
|
+
export const PAIR_VERIFY_MESSAGES = {
|
|
19
|
+
ENCRYPT_SALT: 'Pair-Verify-Encrypt-Salt',
|
|
20
|
+
ENCRYPT_INFO: 'Pair-Verify-Encrypt-Info',
|
|
21
|
+
STATE_03_NONCE: 'PV-Msg03',
|
|
22
|
+
} as const;
|
|
23
|
+
|
|
24
|
+
export const PAIR_VERIFY_STATES = {
|
|
25
|
+
STATE_01: 0x01,
|
|
26
|
+
STATE_02: 0x02,
|
|
27
|
+
STATE_03: 0x03,
|
|
28
|
+
STATE_04: 0x04,
|
|
29
|
+
} as const;
|
|
30
|
+
|
|
31
|
+
export const ENCRYPTION_MESSAGES = {
|
|
32
|
+
CLIENT_ENCRYPT: 'ClientEncrypt-main',
|
|
33
|
+
SERVER_ENCRYPT: 'ServerEncrypt-main',
|
|
34
|
+
} as const;
|
|
35
|
+
|
|
36
|
+
/** Error descriptions for pair verification STATE=4 errors */
|
|
37
|
+
export const PAIR_VERIFY_ERROR_DESCRIPTIONS: Record<number, string> = {
|
|
38
|
+
1: 'Unknown error',
|
|
39
|
+
2: 'Authentication failed - invalid pair record',
|
|
40
|
+
3: 'Backoff - too many attempts',
|
|
41
|
+
4: 'Max peers - device has too many connections',
|
|
42
|
+
5: 'Max tries exceeded',
|
|
43
|
+
6: 'Service unavailable',
|
|
44
|
+
7: 'Device busy',
|
|
45
|
+
} as const;
|
|
46
|
+
|
|
18
47
|
/** TLV type for device info */
|
|
19
48
|
export const INFO_TYPE = 0x11;
|
|
@@ -1,5 +1,16 @@
|
|
|
1
1
|
export { PairingProtocol } from './pairing-protocol.js';
|
|
2
|
-
export {
|
|
2
|
+
export {
|
|
3
|
+
PairVerificationProtocol,
|
|
4
|
+
type VerificationKeys,
|
|
5
|
+
} from './pair-verification-protocol.js';
|
|
6
|
+
export {
|
|
7
|
+
PAIRING_STATES,
|
|
8
|
+
PAIRING_MESSAGES,
|
|
9
|
+
INFO_TYPE,
|
|
10
|
+
PAIR_VERIFY_MESSAGES,
|
|
11
|
+
PAIR_VERIFY_STATES,
|
|
12
|
+
ENCRYPTION_MESSAGES,
|
|
13
|
+
} from './constants.js';
|
|
3
14
|
export type {
|
|
4
15
|
EncryptionKeys,
|
|
5
16
|
PairingRequest,
|