huaweicloud-devkit 1.0.2-dev.1 → 1.0.2-dev.2
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/package.json +1 -1
- package/plugins/huaweicloud-core/.mcp.json +2 -1
- package/plugins/huaweicloud-core/safety/policy.json +16 -1
- package/plugins/huaweicloud-core/safety/rules/cloud-risk-rules.json +21 -0
- package/plugins/huaweicloud-core/skills/huawei-sandbox/SKILL.md +69 -0
- package/plugins/huaweicloud-core/src/auth/agent-registration.mjs +87 -0
- package/plugins/huaweicloud-core/src/auth/credentials.mjs +74 -0
- package/plugins/huaweicloud-core/src/auth/service.mjs +63 -0
- package/plugins/huaweicloud-core/src/sandbox/hdkitservice-api.mjs +97 -0
- package/plugins/huaweicloud-core/src/sandbox/hwlink-api.mjs +153 -0
- package/plugins/huaweicloud-core/src/sandbox/session-manager.mjs +105 -0
- package/plugins/huaweicloud-core/src/setup-cli.mjs +178 -0
- package/plugins/huaweicloud-core/src/tools.mjs +187 -0
- package/plugins/huaweicloud-core/src/ws-exec/hwlink-exec-client.js +427 -0
- package/plugins/huaweicloud-core/src/ws-exec/hwlink-fair-queue.js +132 -0
- package/plugins/huaweicloud-core/src/ws-exec/hwlink-multiplexer.js +227 -0
- package/plugins/huaweicloud-core/src/ws-exec/hwlink-packet.js +202 -0
- package/plugins/huaweicloud-core/src/ws-exec/hwlink-terminal-channel.js +158 -0
- package/plugins/huaweicloud-core/src/ws-exec/index.js +19 -0
- package/plugins/huaweicloud-core/src/ws-exec/ws-exec-client.js +338 -0
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
import { Buffer } from 'node:buffer';
|
|
2
|
+
|
|
3
|
+
import { FairQueue } from './hwlink-fair-queue.js';
|
|
4
|
+
import { formatPacketOneLine, parsePacket } from './hwlink-packet.js';
|
|
5
|
+
|
|
6
|
+
const WS_OPEN = 1;
|
|
7
|
+
|
|
8
|
+
function addWsListener(ws, event, handler) {
|
|
9
|
+
if (typeof ws.on === 'function') {
|
|
10
|
+
ws.on(event, handler);
|
|
11
|
+
return;
|
|
12
|
+
}
|
|
13
|
+
if (typeof ws.addEventListener === 'function') {
|
|
14
|
+
ws.addEventListener(event, handler);
|
|
15
|
+
return;
|
|
16
|
+
}
|
|
17
|
+
throw new Error('WebSocket implementation does not support event listeners');
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function closeQuietly(ws) {
|
|
21
|
+
try {
|
|
22
|
+
ws.close();
|
|
23
|
+
} catch {
|
|
24
|
+
// Close is best-effort during error settlement.
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function extractMessageData(eventOrData) {
|
|
29
|
+
if (
|
|
30
|
+
eventOrData &&
|
|
31
|
+
typeof eventOrData === 'object' &&
|
|
32
|
+
!Buffer.isBuffer(eventOrData) &&
|
|
33
|
+
!(eventOrData instanceof ArrayBuffer) &&
|
|
34
|
+
!ArrayBuffer.isView(eventOrData) &&
|
|
35
|
+
'data' in eventOrData
|
|
36
|
+
) {
|
|
37
|
+
return eventOrData.data;
|
|
38
|
+
}
|
|
39
|
+
return eventOrData;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function eventDataToUint8Array(data) {
|
|
43
|
+
if (data instanceof Uint8Array) return Promise.resolve(data);
|
|
44
|
+
if (Buffer.isBuffer(data)) {
|
|
45
|
+
return Promise.resolve(new Uint8Array(data.buffer, data.byteOffset, data.byteLength));
|
|
46
|
+
}
|
|
47
|
+
if (data instanceof ArrayBuffer) return Promise.resolve(new Uint8Array(data));
|
|
48
|
+
if (ArrayBuffer.isView(data)) {
|
|
49
|
+
return Promise.resolve(new Uint8Array(data.buffer, data.byteOffset, data.byteLength));
|
|
50
|
+
}
|
|
51
|
+
if (data && typeof data.arrayBuffer === 'function') {
|
|
52
|
+
return data.arrayBuffer().then((buffer) => new Uint8Array(buffer));
|
|
53
|
+
}
|
|
54
|
+
return Promise.reject(new Error(`unsupported websocket message data: ${typeof data}`));
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function sendBinary(ws, data) {
|
|
58
|
+
if (ws.readyState !== WS_OPEN) return Promise.resolve();
|
|
59
|
+
|
|
60
|
+
return new Promise((resolve, reject) => {
|
|
61
|
+
let settled = false;
|
|
62
|
+
const done = (err) => {
|
|
63
|
+
if (settled) return;
|
|
64
|
+
settled = true;
|
|
65
|
+
if (err) reject(err);
|
|
66
|
+
else resolve();
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
try {
|
|
70
|
+
const maybePromise = ws.send(data, done);
|
|
71
|
+
if (maybePromise && typeof maybePromise.then === 'function') {
|
|
72
|
+
maybePromise.then(() => done(), done);
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
if (ws.send.length < 2) done();
|
|
76
|
+
} catch (error) {
|
|
77
|
+
done(error);
|
|
78
|
+
}
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
class HwlinkWebSocketMultiplexer {
|
|
83
|
+
constructor(url, source, options = {}) {
|
|
84
|
+
const {
|
|
85
|
+
WebSocketImpl = globalThis.WebSocket,
|
|
86
|
+
protocol = 'devenv',
|
|
87
|
+
onFrame,
|
|
88
|
+
trace = false,
|
|
89
|
+
} = options;
|
|
90
|
+
|
|
91
|
+
if (typeof WebSocketImpl !== 'function') {
|
|
92
|
+
throw new Error('global WebSocket is unavailable; use Node.js 22+ or pass WebSocketImpl');
|
|
93
|
+
}
|
|
94
|
+
if (!Number.isInteger(source) || source < -0x80000000 || source > 0xffffffff) {
|
|
95
|
+
throw new Error('hwlink source must be an int32 or uint32 number');
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
this.url = url;
|
|
99
|
+
this.source = source;
|
|
100
|
+
this.onFrame = onFrame;
|
|
101
|
+
this.trace = trace;
|
|
102
|
+
this.channels = new Map();
|
|
103
|
+
this.unknownIdentifierHandler = null;
|
|
104
|
+
this.onClose = null;
|
|
105
|
+
this.onError = null;
|
|
106
|
+
this.closed = false;
|
|
107
|
+
this.queue = new FairQueue((data) => this.wsSend(data));
|
|
108
|
+
this.ws = new WebSocketImpl(url, protocol);
|
|
109
|
+
|
|
110
|
+
if ('binaryType' in this.ws) {
|
|
111
|
+
this.ws.binaryType = 'arraybuffer';
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
addWsListener(this.ws, 'open', () => {
|
|
115
|
+
for (const ch of this.channels.values()) ch.onopen();
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
addWsListener(this.ws, 'message', (eventOrData) => {
|
|
119
|
+
this.handleMessage(extractMessageData(eventOrData)).catch((error) => {
|
|
120
|
+
this.handleError(error);
|
|
121
|
+
});
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
addWsListener(this.ws, 'error', (eventOrError) => {
|
|
125
|
+
const error = eventOrError instanceof Error
|
|
126
|
+
? eventOrError
|
|
127
|
+
: new Error('hwlink websocket error');
|
|
128
|
+
this.handleError(error);
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
addWsListener(this.ws, 'close', (eventOrCode, maybeReason) => {
|
|
132
|
+
this.closed = true;
|
|
133
|
+
const closeEvent = this.normalizeCloseEvent(eventOrCode, maybeReason);
|
|
134
|
+
for (const ch of this.channels.values()) ch.onclose(closeEvent);
|
|
135
|
+
if (this.onClose) this.onClose(closeEvent);
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
normalizeCloseEvent(eventOrCode, maybeReason) {
|
|
140
|
+
if (typeof eventOrCode === 'number') {
|
|
141
|
+
return {
|
|
142
|
+
code: eventOrCode,
|
|
143
|
+
reason: Buffer.isBuffer(maybeReason) ? maybeReason.toString() : String(maybeReason || ''),
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
return {
|
|
148
|
+
code: eventOrCode && typeof eventOrCode.code === 'number' ? eventOrCode.code : 0,
|
|
149
|
+
reason: eventOrCode && eventOrCode.reason ? String(eventOrCode.reason) : '',
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
register(ch) {
|
|
154
|
+
this.channels.set(ch.identifier, ch);
|
|
155
|
+
this.queue.register(ch);
|
|
156
|
+
if (this.ws.readyState === WS_OPEN) {
|
|
157
|
+
ch.onopen();
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
unregister(ch) {
|
|
162
|
+
this.channels.delete(ch.identifier);
|
|
163
|
+
this.queue.unregister(ch);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
sendFairly(ch, data) {
|
|
167
|
+
this.queue.sendFairly(ch, data);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
sendImmediately(data) {
|
|
171
|
+
this.queue.sendImmediately(data);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
get readyState() {
|
|
175
|
+
return this.ws.readyState;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
async handleMessage(data) {
|
|
179
|
+
const bytes = await eventDataToUint8Array(data);
|
|
180
|
+
const packet = parsePacket(bytes);
|
|
181
|
+
this.reportFrame('in', packet);
|
|
182
|
+
|
|
183
|
+
const ch = this.channels.get(packet.identifier);
|
|
184
|
+
if (ch) {
|
|
185
|
+
ch.onmessage(packet);
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
if (this.unknownIdentifierHandler) {
|
|
190
|
+
this.unknownIdentifierHandler(packet, this);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
wsSend(data) {
|
|
195
|
+
const packet = parsePacket(data);
|
|
196
|
+
this.reportFrame('out', packet);
|
|
197
|
+
return sendBinary(this.ws, data);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
reportFrame(direction, packet) {
|
|
201
|
+
if (this.onFrame) {
|
|
202
|
+
this.onFrame({ direction, packet, line: formatPacketOneLine(packet) });
|
|
203
|
+
}
|
|
204
|
+
if (this.trace) {
|
|
205
|
+
const arrow = direction === 'in' ? '<-' : '->';
|
|
206
|
+
console.error(`${arrow} ${formatPacketOneLine(packet)}`);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
handleError(error) {
|
|
211
|
+
for (const ch of this.channels.values()) ch.onerror(error);
|
|
212
|
+
if (this.onError) this.onError(error);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
close() {
|
|
216
|
+
if (this.closed) return;
|
|
217
|
+
this.closed = true;
|
|
218
|
+
closeQuietly(this.ws);
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
export {
|
|
223
|
+
HwlinkWebSocketMultiplexer,
|
|
224
|
+
addWsListener,
|
|
225
|
+
closeQuietly,
|
|
226
|
+
eventDataToUint8Array,
|
|
227
|
+
};
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
const FIXED_HEADER_LEN = 20;
|
|
2
|
+
const MAX_SEND_CHUNK_SIZE = 8 * 1024;
|
|
3
|
+
|
|
4
|
+
const OpCode = Object.freeze({
|
|
5
|
+
OpCreateTcpTunnel: 0x03,
|
|
6
|
+
OpTcpTunnelData: 0x04,
|
|
7
|
+
OpListenTcpTunnel: 0x05,
|
|
8
|
+
OpReverseCreateTcpTunnel: 0x43,
|
|
9
|
+
OpCmdTerminalResize: 0xfa,
|
|
10
|
+
OpNewCmdTerminal: 0xfb,
|
|
11
|
+
OpCmdTerminalInit: 0xfc,
|
|
12
|
+
OpCmdTerminalData: 0xfd,
|
|
13
|
+
|
|
14
|
+
OpFileSuccess: 0x03 << 8,
|
|
15
|
+
OpSubStreamPing: 0x06 << 8,
|
|
16
|
+
OpSubStreamPong: 0x07 << 8,
|
|
17
|
+
OpTunnelSuccess: 0x08 << 8,
|
|
18
|
+
|
|
19
|
+
OpDisconnect: 0x03 << 16,
|
|
20
|
+
|
|
21
|
+
ErrCmdTerminal: 0x0d << 24,
|
|
22
|
+
ErrTcpTunnel: 0x0e << 24,
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
const ReserveSource = Object.freeze({
|
|
26
|
+
Web: 0x00,
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
let nextId = 1;
|
|
30
|
+
|
|
31
|
+
function nextIdentifier() {
|
|
32
|
+
nextId = ((nextId + 1) & 0xffffffff) >>> 0;
|
|
33
|
+
return nextId;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function isOpTunnelSuccess(op) {
|
|
37
|
+
return (op & 0xff00) === OpCode.OpTunnelSuccess;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function isOpTcpTunnelData(op) {
|
|
41
|
+
return (op & 0xff) === OpCode.OpTcpTunnelData;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function isOpFailed(op) {
|
|
45
|
+
return (op >> 24) !== 0;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function isOpCreateTcpTunnel(op) {
|
|
49
|
+
return (op & 0xff) === OpCode.OpCreateTcpTunnel;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function isOpReverseCreateTcpTunnel(op) {
|
|
53
|
+
return (op & 0xff) === OpCode.OpReverseCreateTcpTunnel;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function isOpCmdTerminalData(op) {
|
|
57
|
+
return (op & 0xff) === OpCode.OpCmdTerminalData;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function isOpListenTcpTunnel(op) {
|
|
61
|
+
return (op & 0xff) === OpCode.OpListenTcpTunnel;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function isSubStreamPing(op) {
|
|
65
|
+
return (op & 0xff00) === OpCode.OpSubStreamPing;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function hex2(n) {
|
|
69
|
+
return `0x${n.toString(16).padStart(2, '0').toUpperCase()}`;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function operationToString(op) {
|
|
73
|
+
return [
|
|
74
|
+
hex2((op >> 24) & 0xff),
|
|
75
|
+
hex2((op >> 16) & 0xff),
|
|
76
|
+
hex2((op >> 8) & 0xff),
|
|
77
|
+
hex2(op & 0xff),
|
|
78
|
+
].join(' ');
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function formatPacketOneLine(packet) {
|
|
82
|
+
const err = (packet.operation >> 24) & 0xff;
|
|
83
|
+
const ctl = (packet.operation >> 16) & 0xff;
|
|
84
|
+
const sync = (packet.operation >> 8) & 0xff;
|
|
85
|
+
const data = packet.operation & 0xff;
|
|
86
|
+
const dataLen = packet.data ? packet.data.length : 0;
|
|
87
|
+
return (
|
|
88
|
+
`[${String(packet.headerLength).padStart(2)}]` +
|
|
89
|
+
`[${hex2(packet.reserve)}]` +
|
|
90
|
+
`[${String(packet.packetLength).padStart(5)}]` +
|
|
91
|
+
`[${hex2(err)} ${hex2(ctl)} ${hex2(sync)} ${hex2(data)}]` +
|
|
92
|
+
`[${String(packet.srcPort).padStart(5)}]` +
|
|
93
|
+
`[${String(packet.dstPort).padStart(5)}]` +
|
|
94
|
+
`[${String(packet.identifier).padStart(10)}]` +
|
|
95
|
+
`[${String(packet.source).padStart(10)}]` +
|
|
96
|
+
`[${String(dataLen).padStart(6)}B]`
|
|
97
|
+
);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function toUint8Array(bytes) {
|
|
101
|
+
if (bytes instanceof Uint8Array) return bytes;
|
|
102
|
+
if (bytes instanceof ArrayBuffer) return new Uint8Array(bytes);
|
|
103
|
+
if (ArrayBuffer.isView(bytes)) {
|
|
104
|
+
return new Uint8Array(bytes.buffer, bytes.byteOffset, bytes.byteLength);
|
|
105
|
+
}
|
|
106
|
+
throw new TypeError('packet bytes must be an ArrayBuffer or Uint8Array');
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function createPacket(opts) {
|
|
110
|
+
const {
|
|
111
|
+
operation,
|
|
112
|
+
reserve = 0,
|
|
113
|
+
srcPort = 0,
|
|
114
|
+
dstPort = 0,
|
|
115
|
+
identifier = 0,
|
|
116
|
+
source = 0,
|
|
117
|
+
payload = null,
|
|
118
|
+
} = opts;
|
|
119
|
+
|
|
120
|
+
const payloadLen = payload ? payload.length : 0;
|
|
121
|
+
const total = FIXED_HEADER_LEN + payloadLen;
|
|
122
|
+
const buf = new ArrayBuffer(total);
|
|
123
|
+
const view = new DataView(buf);
|
|
124
|
+
const u8 = new Uint8Array(buf);
|
|
125
|
+
|
|
126
|
+
view.setUint8(0, FIXED_HEADER_LEN / 4);
|
|
127
|
+
view.setUint8(1, reserve);
|
|
128
|
+
view.setUint16(2, total, false);
|
|
129
|
+
view.setUint32(4, operation, false);
|
|
130
|
+
view.setUint16(8, srcPort, false);
|
|
131
|
+
view.setUint16(10, dstPort, false);
|
|
132
|
+
view.setUint32(12, identifier, false);
|
|
133
|
+
view.setUint32(16, source, false);
|
|
134
|
+
|
|
135
|
+
if (payload && payloadLen) {
|
|
136
|
+
u8.set(payload, FIXED_HEADER_LEN);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
return u8;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function parsePacket(bytes) {
|
|
143
|
+
const u8 = toUint8Array(bytes);
|
|
144
|
+
if (u8.byteLength < FIXED_HEADER_LEN) {
|
|
145
|
+
throw new Error(`packet too short: ${u8.byteLength} bytes`);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
const view = new DataView(u8.buffer, u8.byteOffset, u8.byteLength);
|
|
149
|
+
const headerLength = view.getUint8(0) * 4;
|
|
150
|
+
const reserve = view.getUint8(1);
|
|
151
|
+
const packetLength = view.getUint16(2, false);
|
|
152
|
+
const operation = view.getUint32(4, false);
|
|
153
|
+
const srcPort = view.getUint16(8, false);
|
|
154
|
+
const dstPort = view.getUint16(10, false);
|
|
155
|
+
const identifier = view.getUint32(12, false);
|
|
156
|
+
const source = view.getUint32(16, false);
|
|
157
|
+
|
|
158
|
+
if (headerLength < FIXED_HEADER_LEN) {
|
|
159
|
+
throw new Error(`invalid hwlink packet header length: ${headerLength}`);
|
|
160
|
+
}
|
|
161
|
+
if (packetLength > u8.byteLength) {
|
|
162
|
+
throw new Error(`incomplete hwlink packet: ${packetLength} > ${u8.byteLength}`);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
let data = null;
|
|
166
|
+
if (packetLength > FIXED_HEADER_LEN) {
|
|
167
|
+
data = u8.subarray(FIXED_HEADER_LEN, packetLength);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
return {
|
|
171
|
+
headerLength,
|
|
172
|
+
reserve,
|
|
173
|
+
packetLength,
|
|
174
|
+
operation,
|
|
175
|
+
srcPort,
|
|
176
|
+
dstPort,
|
|
177
|
+
identifier,
|
|
178
|
+
source,
|
|
179
|
+
data,
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
export {
|
|
184
|
+
FIXED_HEADER_LEN,
|
|
185
|
+
MAX_SEND_CHUNK_SIZE,
|
|
186
|
+
OpCode,
|
|
187
|
+
ReserveSource,
|
|
188
|
+
createPacket,
|
|
189
|
+
formatPacketOneLine,
|
|
190
|
+
isOpCmdTerminalData,
|
|
191
|
+
isOpCreateTcpTunnel,
|
|
192
|
+
isOpFailed,
|
|
193
|
+
isOpListenTcpTunnel,
|
|
194
|
+
isOpReverseCreateTcpTunnel,
|
|
195
|
+
isOpTcpTunnelData,
|
|
196
|
+
isOpTunnelSuccess,
|
|
197
|
+
isSubStreamPing,
|
|
198
|
+
nextIdentifier,
|
|
199
|
+
operationToString,
|
|
200
|
+
parsePacket,
|
|
201
|
+
toUint8Array,
|
|
202
|
+
};
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
import {
|
|
2
|
+
OpCode,
|
|
3
|
+
ReserveSource,
|
|
4
|
+
FIXED_HEADER_LEN,
|
|
5
|
+
MAX_SEND_CHUNK_SIZE,
|
|
6
|
+
createPacket,
|
|
7
|
+
isOpCmdTerminalData,
|
|
8
|
+
isOpFailed,
|
|
9
|
+
isSubStreamPing,
|
|
10
|
+
nextIdentifier,
|
|
11
|
+
} from './hwlink-packet.js';
|
|
12
|
+
|
|
13
|
+
const encoder = new TextEncoder();
|
|
14
|
+
const MAX_TERMINAL_PAYLOAD_SIZE = MAX_SEND_CHUNK_SIZE - FIXED_HEADER_LEN;
|
|
15
|
+
|
|
16
|
+
class HwlinkTerminalChannel {
|
|
17
|
+
constructor(username = 'root') {
|
|
18
|
+
this.username = username;
|
|
19
|
+
this.identifier = nextIdentifier();
|
|
20
|
+
this.mux = null;
|
|
21
|
+
this.closed = false;
|
|
22
|
+
this.opened = false;
|
|
23
|
+
this.onDataCb = null;
|
|
24
|
+
this.onCloseCb = null;
|
|
25
|
+
this.onErrorCb = null;
|
|
26
|
+
this.onReadyCb = null;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
onData(cb) {
|
|
30
|
+
this.onDataCb = cb;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
onClose(cb) {
|
|
34
|
+
this.onCloseCb = cb;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
onError(cb) {
|
|
38
|
+
this.onErrorCb = cb;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
onReady(cb) {
|
|
42
|
+
this.onReadyCb = cb;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
get isClosed() {
|
|
46
|
+
return this.closed;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
attach(mux) {
|
|
50
|
+
this.mux = mux;
|
|
51
|
+
mux.register(this);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
onopen() {
|
|
55
|
+
if (this.closed || this.opened) return;
|
|
56
|
+
this.opened = true;
|
|
57
|
+
this.sendRaw(createPacket({
|
|
58
|
+
operation: OpCode.OpNewCmdTerminal,
|
|
59
|
+
reserve: ReserveSource.Web,
|
|
60
|
+
identifier: this.identifier,
|
|
61
|
+
source: this.mux ? this.mux.source : 0,
|
|
62
|
+
payload: encoder.encode(this.username),
|
|
63
|
+
}));
|
|
64
|
+
if (this.onReadyCb) this.onReadyCb();
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
onmessage(packet) {
|
|
68
|
+
if (this.closed) return;
|
|
69
|
+
|
|
70
|
+
if (isOpFailed(packet.operation)) {
|
|
71
|
+
this.handleError(new Error(`hwlink terminal failed: 0x${packet.operation.toString(16)}`));
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
if (isSubStreamPing(packet.operation)) {
|
|
76
|
+
this.sendRaw(createPacket({
|
|
77
|
+
operation: OpCode.OpCmdTerminalData | OpCode.OpSubStreamPong,
|
|
78
|
+
reserve: ReserveSource.Web,
|
|
79
|
+
identifier: this.identifier,
|
|
80
|
+
source: this.mux ? this.mux.source : 0,
|
|
81
|
+
payload: new Uint8Array(0),
|
|
82
|
+
}));
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
if (isOpCmdTerminalData(packet.operation) && packet.data) {
|
|
87
|
+
if (this.onDataCb) this.onDataCb(packet.data);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
onerror(error) {
|
|
92
|
+
if (this.onErrorCb) this.onErrorCb(error);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
onclose() {
|
|
96
|
+
this.close();
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
sendInput(data) {
|
|
100
|
+
if (this.closed) return;
|
|
101
|
+
for (let offset = 0; offset < data.byteLength; offset += MAX_TERMINAL_PAYLOAD_SIZE) {
|
|
102
|
+
const payload = data.subarray(offset, offset + MAX_TERMINAL_PAYLOAD_SIZE);
|
|
103
|
+
this.sendTerminalData(payload);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
sendTerminalData(payload) {
|
|
108
|
+
this.sendRaw(createPacket({
|
|
109
|
+
operation: OpCode.OpCmdTerminalData | OpCode.OpSubStreamPong,
|
|
110
|
+
reserve: ReserveSource.Web,
|
|
111
|
+
identifier: this.identifier,
|
|
112
|
+
source: this.mux ? this.mux.source : 0,
|
|
113
|
+
payload,
|
|
114
|
+
}));
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
sendText(text) {
|
|
118
|
+
this.sendInput(encoder.encode(text));
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
resize(cols, rows) {
|
|
122
|
+
if (this.closed) return;
|
|
123
|
+
const payload = new Uint8Array(4);
|
|
124
|
+
const view = new DataView(payload.buffer);
|
|
125
|
+
view.setUint16(0, rows, false);
|
|
126
|
+
view.setUint16(2, cols, false);
|
|
127
|
+
this.sendRaw(createPacket({
|
|
128
|
+
operation: OpCode.OpCmdTerminalResize,
|
|
129
|
+
reserve: ReserveSource.Web,
|
|
130
|
+
identifier: this.identifier,
|
|
131
|
+
source: this.mux ? this.mux.source : 0,
|
|
132
|
+
payload,
|
|
133
|
+
}));
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
sendRaw(data) {
|
|
137
|
+
if (this.mux) this.mux.sendFairly(this, data);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
handleError(error) {
|
|
141
|
+
if (this.closed) return;
|
|
142
|
+
this.closed = true;
|
|
143
|
+
if (this.mux) this.mux.unregister(this);
|
|
144
|
+
if (this.onErrorCb) this.onErrorCb(error);
|
|
145
|
+
if (this.onCloseCb) this.onCloseCb();
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
close() {
|
|
149
|
+
if (this.closed) return;
|
|
150
|
+
this.closed = true;
|
|
151
|
+
if (this.mux) this.mux.unregister(this);
|
|
152
|
+
if (this.onCloseCb) this.onCloseCb();
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
export {
|
|
157
|
+
HwlinkTerminalChannel,
|
|
158
|
+
};
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import * as hwlinkExec from './hwlink-exec-client.js';
|
|
2
|
+
import * as hwlinkPacket from './hwlink-packet.js';
|
|
3
|
+
import * as wsExec from './ws-exec-client.js';
|
|
4
|
+
import { HwlinkWebSocketMultiplexer } from './hwlink-multiplexer.js';
|
|
5
|
+
import { HwlinkTerminalChannel } from './hwlink-terminal-channel.js';
|
|
6
|
+
|
|
7
|
+
export * from './ws-exec-client.js';
|
|
8
|
+
export * from './hwlink-exec-client.js';
|
|
9
|
+
export { HwlinkTerminalChannel } from './hwlink-terminal-channel.js';
|
|
10
|
+
export { HwlinkWebSocketMultiplexer } from './hwlink-multiplexer.js';
|
|
11
|
+
export { hwlinkPacket };
|
|
12
|
+
|
|
13
|
+
export default {
|
|
14
|
+
...wsExec,
|
|
15
|
+
...hwlinkExec,
|
|
16
|
+
HwlinkTerminalChannel,
|
|
17
|
+
HwlinkWebSocketMultiplexer,
|
|
18
|
+
hwlinkPacket,
|
|
19
|
+
};
|