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,185 @@
|
|
|
1
|
+
import { logger } from '@appium/support';
|
|
2
|
+
import { EventEmitter } from 'events';
|
|
3
|
+
import { type Socket, createConnection } from 'net';
|
|
4
|
+
import type { PacketConsumer, PacketData } from 'tuntap-bridge';
|
|
5
|
+
|
|
6
|
+
const log = logger.getLogger('PacketStreamClient');
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Constants for packet stream protocol
|
|
10
|
+
*/
|
|
11
|
+
const PACKET_LENGTH_PREFIX_SIZE = 10;
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Client that connects to a PacketStreamServer to receive packet data
|
|
15
|
+
* Implements the PacketSource interface required by SyslogService
|
|
16
|
+
*/
|
|
17
|
+
export class PacketStreamClient extends EventEmitter {
|
|
18
|
+
private socket: Socket | null = null;
|
|
19
|
+
private readonly packetConsumers: Set<PacketConsumer> = new Set();
|
|
20
|
+
private buffer: Buffer = Buffer.alloc(0);
|
|
21
|
+
private connected = false;
|
|
22
|
+
|
|
23
|
+
constructor(
|
|
24
|
+
private readonly host: string,
|
|
25
|
+
private readonly port: number,
|
|
26
|
+
) {
|
|
27
|
+
super();
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
async connect(): Promise<void> {
|
|
31
|
+
if (this.connected) {
|
|
32
|
+
log.info('Already connected');
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
return new Promise((resolve, reject) => {
|
|
37
|
+
this.socket = createConnection(
|
|
38
|
+
{ host: this.host, port: this.port },
|
|
39
|
+
() => {
|
|
40
|
+
log.info(
|
|
41
|
+
`Connected to packet stream server at ${this.host}:${this.port}`,
|
|
42
|
+
);
|
|
43
|
+
this.connected = true;
|
|
44
|
+
resolve();
|
|
45
|
+
},
|
|
46
|
+
);
|
|
47
|
+
|
|
48
|
+
this.socket.on('data', (data) => {
|
|
49
|
+
this.handleData(data);
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
this.socket.once('close', () => {
|
|
53
|
+
log.info('Disconnected from packet stream server');
|
|
54
|
+
this.connected = false;
|
|
55
|
+
this.emit('close');
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
this.socket.on('error', (err) => {
|
|
59
|
+
log.error(`Socket error: ${err}`);
|
|
60
|
+
this.connected = false;
|
|
61
|
+
if (!this.socket) {
|
|
62
|
+
reject(err);
|
|
63
|
+
} else {
|
|
64
|
+
this.emit('error', err);
|
|
65
|
+
}
|
|
66
|
+
});
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
isConnected(): boolean {
|
|
71
|
+
return this.connected;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
async disconnect(): Promise<void> {
|
|
75
|
+
if (this.socket && !this.socket.destroyed) {
|
|
76
|
+
this.socket.destroy();
|
|
77
|
+
this.socket = null;
|
|
78
|
+
}
|
|
79
|
+
this.connected = false;
|
|
80
|
+
this.packetConsumers.clear();
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
addPacketConsumer(consumer: PacketConsumer): void {
|
|
84
|
+
this.packetConsumers.add(consumer);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
removePacketConsumer(consumer: PacketConsumer): void {
|
|
88
|
+
this.packetConsumers.delete(consumer);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Handle incoming data from the socket
|
|
93
|
+
*/
|
|
94
|
+
private handleData(data: Buffer): void {
|
|
95
|
+
this.buffer = Buffer.concat([this.buffer, data]);
|
|
96
|
+
this.processBuffer();
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Process buffered data to extract complete messages
|
|
101
|
+
*/
|
|
102
|
+
private processBuffer(): void {
|
|
103
|
+
while (this.buffer.length >= PACKET_LENGTH_PREFIX_SIZE) {
|
|
104
|
+
const messageLength = this.extractMessageLength();
|
|
105
|
+
|
|
106
|
+
if (messageLength === null) {
|
|
107
|
+
// Invalid length, reset buffer
|
|
108
|
+
this.buffer = Buffer.alloc(0);
|
|
109
|
+
break;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const totalMessageSize = PACKET_LENGTH_PREFIX_SIZE + messageLength;
|
|
113
|
+
|
|
114
|
+
if (this.buffer.length < totalMessageSize) {
|
|
115
|
+
// Wait for more data
|
|
116
|
+
break;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const messageData = this.buffer.slice(
|
|
120
|
+
PACKET_LENGTH_PREFIX_SIZE,
|
|
121
|
+
totalMessageSize,
|
|
122
|
+
);
|
|
123
|
+
this.buffer = this.buffer.slice(totalMessageSize);
|
|
124
|
+
|
|
125
|
+
this.processMessage(messageData);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Extract message length from buffer
|
|
131
|
+
* @returns Message length or null if invalid
|
|
132
|
+
*/
|
|
133
|
+
private extractMessageLength(): number | null {
|
|
134
|
+
const lengthStr = this.buffer
|
|
135
|
+
.slice(0, PACKET_LENGTH_PREFIX_SIZE)
|
|
136
|
+
.toString();
|
|
137
|
+
const messageLength = parseInt(lengthStr, 10);
|
|
138
|
+
|
|
139
|
+
if (isNaN(messageLength)) {
|
|
140
|
+
log.error('Invalid message length, clearing buffer');
|
|
141
|
+
return null;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
return messageLength;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Process a single message
|
|
149
|
+
*/
|
|
150
|
+
private processMessage(messageData: Buffer): void {
|
|
151
|
+
try {
|
|
152
|
+
const packet = this.parsePacket(messageData);
|
|
153
|
+
this.notifyConsumers(packet);
|
|
154
|
+
} catch (err) {
|
|
155
|
+
log.error(`Error processing message: ${err}`);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Parse packet data from message buffer
|
|
161
|
+
*/
|
|
162
|
+
private parsePacket(messageData: Buffer): PacketData {
|
|
163
|
+
const packet: PacketData = JSON.parse(messageData.toString());
|
|
164
|
+
|
|
165
|
+
// Reconstruct Buffer from JSON
|
|
166
|
+
if (packet.payload && typeof packet.payload === 'object') {
|
|
167
|
+
packet.payload = Buffer.from(packet.payload);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
return packet;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Notify all packet consumers
|
|
175
|
+
*/
|
|
176
|
+
private notifyConsumers(packet: PacketData): void {
|
|
177
|
+
for (const consumer of this.packetConsumers) {
|
|
178
|
+
try {
|
|
179
|
+
consumer.onPacket(packet);
|
|
180
|
+
} catch (err) {
|
|
181
|
+
log.error(`Error in packet consumer: ${err}`);
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
}
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import { logger } from '@appium/support';
|
|
2
|
+
import { EventEmitter } from 'events';
|
|
3
|
+
import { type Server, type Socket, createServer } from 'net';
|
|
4
|
+
import type { PacketConsumer, PacketData } from 'tuntap-bridge';
|
|
5
|
+
|
|
6
|
+
const log = logger.getLogger('PacketStreamServer');
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Interface for serialized packet message
|
|
10
|
+
*/
|
|
11
|
+
interface SerializedPacketMessage {
|
|
12
|
+
length: string;
|
|
13
|
+
data: string;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Server that exposes packet streaming from a tunnel over TCP
|
|
18
|
+
* This allows cross-process access to tunnel packet streams
|
|
19
|
+
*/
|
|
20
|
+
export class PacketStreamServer extends EventEmitter {
|
|
21
|
+
private server: Server | null = null;
|
|
22
|
+
private readonly clients: Set<Socket> = new Set();
|
|
23
|
+
private packetConsumer: PacketConsumer | null = null;
|
|
24
|
+
|
|
25
|
+
constructor(private readonly port: number) {
|
|
26
|
+
super();
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Start the packet stream server
|
|
31
|
+
* @throws {Error} If server is already started
|
|
32
|
+
*/
|
|
33
|
+
async start(): Promise<void> {
|
|
34
|
+
if (this.server) {
|
|
35
|
+
throw new Error('Server already started');
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
this.server = createServer((client) => {
|
|
39
|
+
this.handleClientConnection(client);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
this.packetConsumer = this.createPacketConsumer();
|
|
43
|
+
|
|
44
|
+
return new Promise((resolve, reject) => {
|
|
45
|
+
this.server!.listen(this.port, () => {
|
|
46
|
+
log.info(`Packet stream server listening on port ${this.port}`);
|
|
47
|
+
resolve();
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
this.server!.on('error', reject);
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
async stop(): Promise<void> {
|
|
55
|
+
for (const client of this.clients) {
|
|
56
|
+
client.destroy();
|
|
57
|
+
}
|
|
58
|
+
this.clients.clear();
|
|
59
|
+
|
|
60
|
+
if (this.server) {
|
|
61
|
+
return new Promise((resolve) => {
|
|
62
|
+
this.server?.close(() => {
|
|
63
|
+
this.server = null;
|
|
64
|
+
resolve();
|
|
65
|
+
});
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
getPacketConsumer(): PacketConsumer | null {
|
|
71
|
+
return this.packetConsumer;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Handle new client connection
|
|
76
|
+
*/
|
|
77
|
+
private handleClientConnection(client: Socket): void {
|
|
78
|
+
log.info(`Client connected from ${client.remoteAddress}`);
|
|
79
|
+
this.clients.add(client);
|
|
80
|
+
|
|
81
|
+
client.on('close', () => {
|
|
82
|
+
log.info(`Client disconnected from ${client.remoteAddress}`);
|
|
83
|
+
this.clients.delete(client);
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
client.on('error', (err) => {
|
|
87
|
+
log.error(`Client error: ${err}`);
|
|
88
|
+
this.clients.delete(client);
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Create packet consumer that broadcasts packets to all connected clients
|
|
94
|
+
*/
|
|
95
|
+
private createPacketConsumer(): PacketConsumer {
|
|
96
|
+
return {
|
|
97
|
+
onPacket: (packet: PacketData) => {
|
|
98
|
+
this.broadcastPacket(packet);
|
|
99
|
+
},
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Broadcast packet to all connected clients
|
|
105
|
+
*/
|
|
106
|
+
private broadcastPacket(packet: PacketData): void {
|
|
107
|
+
try {
|
|
108
|
+
const serialized = JSON.stringify(packet);
|
|
109
|
+
const message = this.createMessage(serialized);
|
|
110
|
+
|
|
111
|
+
for (const client of this.clients) {
|
|
112
|
+
if (!client.destroyed) {
|
|
113
|
+
client.write(message, (err) => {
|
|
114
|
+
if (err) {
|
|
115
|
+
log.error(`Failed to write to client: ${err}`);
|
|
116
|
+
this.clients.delete(client);
|
|
117
|
+
}
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
} catch (err) {
|
|
122
|
+
log.error(`Failed to broadcast packet: ${err}`);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Create a message buffer with length prefix
|
|
128
|
+
*/
|
|
129
|
+
private createMessage(data: string): Buffer {
|
|
130
|
+
const lengthPrefix = data.length.toString().padStart(10, '0');
|
|
131
|
+
return Buffer.concat([Buffer.from(lengthPrefix), Buffer.from(data)]);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
import { logger } from '@appium/support';
|
|
2
|
+
|
|
3
|
+
import type { TunnelRegistry, TunnelRegistryEntry } from '../types.js';
|
|
4
|
+
|
|
5
|
+
const log = logger.getLogger('TunnelApiClient');
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* API client for tunnel registry operations
|
|
9
|
+
* This client handles communication with the API server for tunnel data
|
|
10
|
+
*/
|
|
11
|
+
export class TunnelApiClient {
|
|
12
|
+
private apiBaseUrl: string;
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Create a new TunnelApiClient
|
|
16
|
+
* @param apiBaseUrl - Base URL for the API server
|
|
17
|
+
*/
|
|
18
|
+
constructor(apiBaseUrl: string = 'http://localhost:42314/remotexpc/tunnels') {
|
|
19
|
+
this.apiBaseUrl = apiBaseUrl;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Set the API base URL
|
|
24
|
+
* @param url - New base URL for the API server
|
|
25
|
+
*/
|
|
26
|
+
setApiBaseUrl(url: string): void {
|
|
27
|
+
this.apiBaseUrl = url;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Get the API base URL
|
|
32
|
+
* @returns The current API base URL
|
|
33
|
+
*/
|
|
34
|
+
getApiBaseUrl(): string {
|
|
35
|
+
return this.apiBaseUrl;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Fetch all tunnel registry data from the API server
|
|
40
|
+
* @returns The complete tunnel registry
|
|
41
|
+
*/
|
|
42
|
+
async fetchRegistry(): Promise<TunnelRegistry> {
|
|
43
|
+
try {
|
|
44
|
+
const response = await fetch(this.apiBaseUrl);
|
|
45
|
+
|
|
46
|
+
if (!response.ok) {
|
|
47
|
+
throw new Error(`API request failed with status: ${response.status}`);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
return (await response.json()) as TunnelRegistry;
|
|
51
|
+
} catch (error) {
|
|
52
|
+
log.warn(`Failed to fetch tunnel registry from API: ${error}`);
|
|
53
|
+
// Return empty registry as fallback
|
|
54
|
+
return {
|
|
55
|
+
tunnels: {},
|
|
56
|
+
metadata: {
|
|
57
|
+
lastUpdated: new Date().toISOString(),
|
|
58
|
+
totalTunnels: 0,
|
|
59
|
+
activeTunnels: 0,
|
|
60
|
+
},
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Get a specific tunnel by UDID
|
|
67
|
+
* @param udid - Device UDID
|
|
68
|
+
* @returns Tunnel registry entry or null if not found
|
|
69
|
+
*/
|
|
70
|
+
async getTunnelByUdid(udid: string): Promise<TunnelRegistryEntry | null> {
|
|
71
|
+
try {
|
|
72
|
+
const response = await fetch(`${this.apiBaseUrl}/${udid}`);
|
|
73
|
+
|
|
74
|
+
if (response.status === 404) {
|
|
75
|
+
return null;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
if (!response.ok) {
|
|
79
|
+
throw new Error(`API request failed with status: ${response.status}`);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
return (await response.json()) as TunnelRegistryEntry;
|
|
83
|
+
} catch (error) {
|
|
84
|
+
log.warn(`Failed to fetch tunnel for UDID ${udid}: ${error}`);
|
|
85
|
+
return null;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Get tunnel by device ID
|
|
91
|
+
* @param deviceId - Device ID
|
|
92
|
+
* @returns Tunnel registry entry or null if not found
|
|
93
|
+
*/
|
|
94
|
+
async getTunnelByDeviceId(
|
|
95
|
+
deviceId: number,
|
|
96
|
+
): Promise<TunnelRegistryEntry | null> {
|
|
97
|
+
try {
|
|
98
|
+
const response = await fetch(`${this.apiBaseUrl}/device/${deviceId}`);
|
|
99
|
+
|
|
100
|
+
if (response.status === 404) {
|
|
101
|
+
return null;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
if (!response.ok) {
|
|
105
|
+
throw new Error(`API request failed with status: ${response.status}`);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
return (await response.json()) as TunnelRegistryEntry;
|
|
109
|
+
} catch (error) {
|
|
110
|
+
log.warn(`Failed to fetch tunnel for device ID ${deviceId}: ${error}`);
|
|
111
|
+
return null;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Get all tunnels
|
|
117
|
+
* @returns Array of tunnel registry entries
|
|
118
|
+
*/
|
|
119
|
+
async getAllTunnels(): Promise<TunnelRegistryEntry[]> {
|
|
120
|
+
try {
|
|
121
|
+
const registry = await this.fetchRegistry();
|
|
122
|
+
return Object.values(registry.tunnels);
|
|
123
|
+
} catch (error) {
|
|
124
|
+
log.warn(`Failed to fetch all tunnels: ${error}`);
|
|
125
|
+
return [];
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Check if a tunnel exists for a specific UDID
|
|
131
|
+
* @param udid - Device UDID
|
|
132
|
+
* @returns True if tunnel exists, false otherwise
|
|
133
|
+
*/
|
|
134
|
+
async hasTunnel(udid: string): Promise<boolean> {
|
|
135
|
+
const tunnel = await this.getTunnelByUdid(udid);
|
|
136
|
+
return tunnel !== null;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Get registry metadata
|
|
141
|
+
* @returns Registry metadata
|
|
142
|
+
*/
|
|
143
|
+
async getMetadata(): Promise<TunnelRegistry['metadata']> {
|
|
144
|
+
try {
|
|
145
|
+
const registry = await this.fetchRegistry();
|
|
146
|
+
return registry.metadata;
|
|
147
|
+
} catch (error) {
|
|
148
|
+
log.warn(`Failed to fetch registry metadata: ${error}`);
|
|
149
|
+
return {
|
|
150
|
+
lastUpdated: new Date().toISOString(),
|
|
151
|
+
totalTunnels: 0,
|
|
152
|
+
activeTunnels: 0,
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Get tunnel connection details formatted for easy use
|
|
159
|
+
* @param udid - Device UDID
|
|
160
|
+
* @returns Connection details or null if tunnel not found
|
|
161
|
+
*/
|
|
162
|
+
async getTunnelConnection(udid: string): Promise<{
|
|
163
|
+
host: string;
|
|
164
|
+
port: number;
|
|
165
|
+
udid: string;
|
|
166
|
+
packetStreamPort: number;
|
|
167
|
+
} | null> {
|
|
168
|
+
const tunnel = await this.getTunnelByUdid(udid);
|
|
169
|
+
if (!tunnel) {
|
|
170
|
+
return null;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
return {
|
|
174
|
+
host: tunnel.address,
|
|
175
|
+
port: tunnel.rsdPort,
|
|
176
|
+
udid: tunnel.udid,
|
|
177
|
+
packetStreamPort: tunnel.packetStreamPort,
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* List all available device UDIDs with tunnels
|
|
183
|
+
* @returns Array of device UDIDs
|
|
184
|
+
*/
|
|
185
|
+
async getAvailableDevices(): Promise<string[]> {
|
|
186
|
+
try {
|
|
187
|
+
const registry = await this.fetchRegistry();
|
|
188
|
+
return Object.keys(registry.tunnels);
|
|
189
|
+
} catch (error) {
|
|
190
|
+
log.warn(`Failed to fetch available devices: ${error}`);
|
|
191
|
+
return [];
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* Update or create a tunnel entry
|
|
197
|
+
* @param entry - Tunnel registry entry to update or create
|
|
198
|
+
* @returns True if successful, false otherwise
|
|
199
|
+
*/
|
|
200
|
+
async updateTunnel(entry: TunnelRegistryEntry): Promise<boolean> {
|
|
201
|
+
try {
|
|
202
|
+
const response = await fetch(`${this.apiBaseUrl}/${entry.udid}`, {
|
|
203
|
+
method: 'PUT',
|
|
204
|
+
headers: {
|
|
205
|
+
'Content-Type': 'application/json',
|
|
206
|
+
},
|
|
207
|
+
body: JSON.stringify(entry),
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
return response.ok;
|
|
211
|
+
} catch (error) {
|
|
212
|
+
log.error(`Failed to update tunnel for UDID ${entry.udid}: ${error}`);
|
|
213
|
+
return false;
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* Delete a tunnel entry
|
|
219
|
+
* @param udid - Device UDID
|
|
220
|
+
* @returns True if successful, false otherwise
|
|
221
|
+
*/
|
|
222
|
+
async deleteTunnel(udid: string): Promise<boolean> {
|
|
223
|
+
try {
|
|
224
|
+
const response = await fetch(`${this.apiBaseUrl}/${udid}`, {
|
|
225
|
+
method: 'DELETE',
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
return response.ok;
|
|
229
|
+
} catch (error) {
|
|
230
|
+
log.error(`Failed to delete tunnel for UDID ${udid}: ${error}`);
|
|
231
|
+
return false;
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
}
|