@serverkgg/bridge 0.0.13 → 0.0.14
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/src/context/bridgeContext.ts +9 -2
- package/src/manifest/manifest.ts +1 -1
- package/src/modules/bridgeModule.ts +2 -1
- package/src/rcon/index.ts +3 -2
- package/src/rcon/rconAccess.test.ts +286 -0
- package/src/rcon/rconAccess.ts +252 -0
- package/src/rcon/rconClient.test.ts +1544 -0
- package/src/rcon/rconClient.ts +275 -0
- package/src/rcon/rconError.ts +21 -0
- package/src/rcon/rconFrame.test.ts +269 -0
- package/src/rcon/{sourceRconFrame.ts → rconFrame.ts} +23 -14
- package/src/rcon/rconIds.test.ts +59 -0
- package/src/rcon/rconIds.ts +33 -0
- package/src/rcon/rconLink.ts +427 -0
- package/src/rcon/rconWriter.test.ts +95 -0
- package/src/rcon/rconWriter.ts +63 -0
- package/src/runtime/bridgeConsoleHandler.test.ts +196 -0
- package/src/runtime/bridgeConsoleHandler.ts +26 -0
- package/src/runtime/bridgeRcon.ts +6 -2
- package/src/runtime/serve.ts +10 -1
- package/src/rcon/sourceRcon.test.ts +0 -308
- package/src/rcon/sourceRcon.ts +0 -276
- package/src/rcon/sourceRconFrame.test.ts +0 -152
package/src/rcon/sourceRcon.ts
DELETED
|
@@ -1,276 +0,0 @@
|
|
|
1
|
-
import { BridgeRconCode, BridgeRconError } from "../core";
|
|
2
|
-
import {
|
|
3
|
-
concatBytes,
|
|
4
|
-
decodeSourceRconFrames,
|
|
5
|
-
encodeSourceRconFrame,
|
|
6
|
-
SOURCE_RCON_AUTH_FAILED_ID,
|
|
7
|
-
type SourceRconFrame,
|
|
8
|
-
SourceRconRequest,
|
|
9
|
-
SourceRconResponse,
|
|
10
|
-
} from "./sourceRconFrame";
|
|
11
|
-
|
|
12
|
-
const DEFAULT_HOST = "127.0.0.1";
|
|
13
|
-
|
|
14
|
-
const DEFAULT_TIMEOUT_MS = 10_000;
|
|
15
|
-
|
|
16
|
-
const AUTH_ID = 1;
|
|
17
|
-
|
|
18
|
-
const RESPONSE_LIMIT = 1024 * 1024;
|
|
19
|
-
|
|
20
|
-
export interface SourceRconOptions {
|
|
21
|
-
host?: string;
|
|
22
|
-
port: number;
|
|
23
|
-
password: string;
|
|
24
|
-
timeoutMs?: number;
|
|
25
|
-
multiPacket?: boolean;
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
export interface SourceRconClient {
|
|
29
|
-
command(text: string): Promise<string>;
|
|
30
|
-
close(): void;
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
interface Waiter {
|
|
34
|
-
id: number;
|
|
35
|
-
auth: boolean;
|
|
36
|
-
probeId: number | null;
|
|
37
|
-
parts: string[];
|
|
38
|
-
settle(outcome: string | BridgeRconError): void;
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
const rconError = (code: BridgeRconCode, message: string, cause?: unknown) => {
|
|
42
|
-
if (cause instanceof BridgeRconError) {
|
|
43
|
-
return cause;
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
const detail = cause instanceof Error && cause.message.length > 0 ? ` — ${cause.message}` : "";
|
|
47
|
-
|
|
48
|
-
return new BridgeRconError(code, `${message}${detail}`);
|
|
49
|
-
};
|
|
50
|
-
|
|
51
|
-
export const sourceRcon = async (options: SourceRconOptions): Promise<SourceRconClient> => {
|
|
52
|
-
const host = options.host ?? DEFAULT_HOST;
|
|
53
|
-
const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
54
|
-
const multiPacket = options.multiPacket !== false;
|
|
55
|
-
const target = `${host}:${options.port}`;
|
|
56
|
-
|
|
57
|
-
let buffer: Uint8Array<ArrayBufferLike> = new Uint8Array(0);
|
|
58
|
-
let waiter: Waiter | null = null;
|
|
59
|
-
let failure: BridgeRconError | null = null;
|
|
60
|
-
let nextId = AUTH_ID + 1;
|
|
61
|
-
let queue: Promise<unknown> = Promise.resolve();
|
|
62
|
-
|
|
63
|
-
const fail = (error: BridgeRconError) => {
|
|
64
|
-
failure ??= error;
|
|
65
|
-
|
|
66
|
-
const waiting = waiter;
|
|
67
|
-
|
|
68
|
-
waiter = null;
|
|
69
|
-
waiting?.settle(error);
|
|
70
|
-
};
|
|
71
|
-
|
|
72
|
-
const accept = (frame: SourceRconFrame) => {
|
|
73
|
-
const waiting = waiter;
|
|
74
|
-
|
|
75
|
-
if (!waiting) {
|
|
76
|
-
return;
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
if (waiting.auth) {
|
|
80
|
-
if (frame.type !== SourceRconResponse.Auth) {
|
|
81
|
-
return;
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
waiter = null;
|
|
85
|
-
|
|
86
|
-
waiting.settle(
|
|
87
|
-
frame.id === SOURCE_RCON_AUTH_FAILED_ID
|
|
88
|
-
? new BridgeRconError(BridgeRconCode.AuthFailed, `${target} rejected the rcon password`)
|
|
89
|
-
: "",
|
|
90
|
-
);
|
|
91
|
-
|
|
92
|
-
return;
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
if (frame.type !== SourceRconResponse.Value) {
|
|
96
|
-
return;
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
if (waiting.probeId !== null && frame.id === waiting.probeId) {
|
|
100
|
-
waiter = null;
|
|
101
|
-
waiting.settle(waiting.parts.join(""));
|
|
102
|
-
|
|
103
|
-
return;
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
if (frame.id !== waiting.id) {
|
|
107
|
-
return;
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
waiting.parts.push(frame.body);
|
|
111
|
-
|
|
112
|
-
if (waiting.parts.reduce((total, part) => total + part.length, 0) > RESPONSE_LIMIT) {
|
|
113
|
-
fail(
|
|
114
|
-
new BridgeRconError(
|
|
115
|
-
BridgeRconCode.Malformed,
|
|
116
|
-
`${target} sent an rcon reply larger than ${RESPONSE_LIMIT} bytes`,
|
|
117
|
-
),
|
|
118
|
-
);
|
|
119
|
-
|
|
120
|
-
return;
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
if (waiting.probeId === null) {
|
|
124
|
-
waiter = null;
|
|
125
|
-
waiting.settle(waiting.parts.join(""));
|
|
126
|
-
}
|
|
127
|
-
};
|
|
128
|
-
|
|
129
|
-
const receive = (chunk: Uint8Array) => {
|
|
130
|
-
buffer = concatBytes(buffer, chunk);
|
|
131
|
-
|
|
132
|
-
try {
|
|
133
|
-
const decoded = decodeSourceRconFrames(buffer);
|
|
134
|
-
|
|
135
|
-
buffer = decoded.rest;
|
|
136
|
-
|
|
137
|
-
for (const frame of decoded.frames) {
|
|
138
|
-
accept(frame);
|
|
139
|
-
}
|
|
140
|
-
} catch (error) {
|
|
141
|
-
fail(rconError(BridgeRconCode.Malformed, `${target} sent an unreadable rcon packet`, error));
|
|
142
|
-
}
|
|
143
|
-
};
|
|
144
|
-
|
|
145
|
-
const connect = async () => {
|
|
146
|
-
let guard: ReturnType<typeof setTimeout> | undefined;
|
|
147
|
-
|
|
148
|
-
try {
|
|
149
|
-
return await Promise.race([
|
|
150
|
-
Bun.connect({
|
|
151
|
-
hostname: host,
|
|
152
|
-
port: options.port,
|
|
153
|
-
socket: {
|
|
154
|
-
data(_socket, data) {
|
|
155
|
-
receive(data);
|
|
156
|
-
},
|
|
157
|
-
close() {
|
|
158
|
-
fail(new BridgeRconError(BridgeRconCode.Closed, `${target} closed the rcon connection`));
|
|
159
|
-
},
|
|
160
|
-
error(_socket, error) {
|
|
161
|
-
fail(rconError(BridgeRconCode.Unreachable, `the rcon connection to ${target} failed`, error));
|
|
162
|
-
},
|
|
163
|
-
},
|
|
164
|
-
}),
|
|
165
|
-
new Promise<never>((_resolve, reject) => {
|
|
166
|
-
guard = setTimeout(() => {
|
|
167
|
-
reject(new BridgeRconError(BridgeRconCode.TimedOut, `the rcon connection to ${target} timed out`));
|
|
168
|
-
}, timeoutMs);
|
|
169
|
-
}),
|
|
170
|
-
]);
|
|
171
|
-
} catch (error) {
|
|
172
|
-
throw rconError(BridgeRconCode.Unreachable, `the rcon connection to ${target} could not be opened`, error);
|
|
173
|
-
} finally {
|
|
174
|
-
clearTimeout(guard);
|
|
175
|
-
}
|
|
176
|
-
};
|
|
177
|
-
|
|
178
|
-
const socket = await connect();
|
|
179
|
-
|
|
180
|
-
const exchange = (id: number, type: SourceRconRequest, body: string, probeId: number | null) => {
|
|
181
|
-
return new Promise<string>((resolve, reject) => {
|
|
182
|
-
if (failure) {
|
|
183
|
-
reject(failure);
|
|
184
|
-
|
|
185
|
-
return;
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
let settled = false;
|
|
189
|
-
|
|
190
|
-
const timer = setTimeout(() => {
|
|
191
|
-
fail(new BridgeRconError(BridgeRconCode.TimedOut, `${target} did not answer the rcon request in time`));
|
|
192
|
-
socket.end();
|
|
193
|
-
}, timeoutMs);
|
|
194
|
-
|
|
195
|
-
waiter = {
|
|
196
|
-
id,
|
|
197
|
-
auth: type === SourceRconRequest.Auth,
|
|
198
|
-
probeId,
|
|
199
|
-
parts: [],
|
|
200
|
-
settle(outcome) {
|
|
201
|
-
if (settled) {
|
|
202
|
-
return;
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
settled = true;
|
|
206
|
-
|
|
207
|
-
clearTimeout(timer);
|
|
208
|
-
|
|
209
|
-
if (outcome instanceof BridgeRconError) {
|
|
210
|
-
reject(outcome);
|
|
211
|
-
|
|
212
|
-
return;
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
resolve(outcome);
|
|
216
|
-
},
|
|
217
|
-
};
|
|
218
|
-
|
|
219
|
-
try {
|
|
220
|
-
socket.write(
|
|
221
|
-
encodeSourceRconFrame({
|
|
222
|
-
id,
|
|
223
|
-
type,
|
|
224
|
-
body,
|
|
225
|
-
}),
|
|
226
|
-
);
|
|
227
|
-
|
|
228
|
-
if (probeId !== null) {
|
|
229
|
-
socket.write(
|
|
230
|
-
encodeSourceRconFrame({
|
|
231
|
-
id: probeId,
|
|
232
|
-
type: SourceRconResponse.Value,
|
|
233
|
-
body: "",
|
|
234
|
-
}),
|
|
235
|
-
);
|
|
236
|
-
}
|
|
237
|
-
} catch (error) {
|
|
238
|
-
fail(rconError(BridgeRconCode.Closed, `the rcon request to ${target} could not be sent`, error));
|
|
239
|
-
}
|
|
240
|
-
});
|
|
241
|
-
};
|
|
242
|
-
|
|
243
|
-
const serialize = <Result>(work: () => Promise<Result>): Promise<Result> => {
|
|
244
|
-
const next = queue.then(work, work);
|
|
245
|
-
|
|
246
|
-
queue = next.then(
|
|
247
|
-
() => undefined,
|
|
248
|
-
() => undefined,
|
|
249
|
-
);
|
|
250
|
-
|
|
251
|
-
return next;
|
|
252
|
-
};
|
|
253
|
-
|
|
254
|
-
try {
|
|
255
|
-
await serialize(() => exchange(AUTH_ID, SourceRconRequest.Auth, options.password, null));
|
|
256
|
-
} catch (error) {
|
|
257
|
-
socket.end();
|
|
258
|
-
|
|
259
|
-
throw error;
|
|
260
|
-
}
|
|
261
|
-
|
|
262
|
-
return {
|
|
263
|
-
async command(text) {
|
|
264
|
-
return await serialize(async () => {
|
|
265
|
-
const id = nextId++;
|
|
266
|
-
const probeId = multiPacket ? nextId++ : null;
|
|
267
|
-
|
|
268
|
-
return await exchange(id, SourceRconRequest.Command, text, probeId);
|
|
269
|
-
});
|
|
270
|
-
},
|
|
271
|
-
|
|
272
|
-
close() {
|
|
273
|
-
socket.end();
|
|
274
|
-
},
|
|
275
|
-
};
|
|
276
|
-
};
|
|
@@ -1,152 +0,0 @@
|
|
|
1
|
-
import { describe, expect, test } from "bun:test";
|
|
2
|
-
import { BridgeRconCode, BridgeRconError } from "../core";
|
|
3
|
-
import {
|
|
4
|
-
concatBytes,
|
|
5
|
-
decodeSourceRconFrames,
|
|
6
|
-
encodeSourceRconFrame,
|
|
7
|
-
SOURCE_RCON_BODY_LIMIT,
|
|
8
|
-
SourceRconRequest,
|
|
9
|
-
SourceRconResponse,
|
|
10
|
-
} from "./sourceRconFrame";
|
|
11
|
-
|
|
12
|
-
describe("encodeSourceRconFrame", () => {
|
|
13
|
-
test("writes size, id and type as little endian int32 followed by two nulls", () => {
|
|
14
|
-
const packet = encodeSourceRconFrame({
|
|
15
|
-
id: 7,
|
|
16
|
-
type: SourceRconRequest.Auth,
|
|
17
|
-
body: "pass",
|
|
18
|
-
});
|
|
19
|
-
|
|
20
|
-
const view = new DataView(packet.buffer);
|
|
21
|
-
|
|
22
|
-
expect(packet).toHaveLength(18);
|
|
23
|
-
expect(view.getInt32(0, true)).toBe(14);
|
|
24
|
-
expect(view.getInt32(4, true)).toBe(7);
|
|
25
|
-
expect(view.getInt32(8, true)).toBe(3);
|
|
26
|
-
expect(new TextDecoder().decode(packet.subarray(12, 16))).toBe("pass");
|
|
27
|
-
expect(packet.at(16)).toBe(0);
|
|
28
|
-
expect(packet.at(17)).toBe(0);
|
|
29
|
-
});
|
|
30
|
-
|
|
31
|
-
test("refuses a body above the 4096 byte cap", () => {
|
|
32
|
-
const error = (() => {
|
|
33
|
-
try {
|
|
34
|
-
encodeSourceRconFrame({
|
|
35
|
-
id: 1,
|
|
36
|
-
type: SourceRconRequest.Command,
|
|
37
|
-
body: "x".repeat(SOURCE_RCON_BODY_LIMIT + 1),
|
|
38
|
-
});
|
|
39
|
-
|
|
40
|
-
return null;
|
|
41
|
-
} catch (reason: unknown) {
|
|
42
|
-
return reason;
|
|
43
|
-
}
|
|
44
|
-
})();
|
|
45
|
-
|
|
46
|
-
expect(error).toBeInstanceOf(BridgeRconError);
|
|
47
|
-
expect((error as BridgeRconError).code).toBe(BridgeRconCode.Oversized);
|
|
48
|
-
});
|
|
49
|
-
});
|
|
50
|
-
|
|
51
|
-
describe("decodeSourceRconFrames", () => {
|
|
52
|
-
test("round-trips a frame", () => {
|
|
53
|
-
const decoded = decodeSourceRconFrames(
|
|
54
|
-
encodeSourceRconFrame({
|
|
55
|
-
id: 3,
|
|
56
|
-
type: SourceRconResponse.Value,
|
|
57
|
-
body: "hello",
|
|
58
|
-
}),
|
|
59
|
-
);
|
|
60
|
-
|
|
61
|
-
expect(decoded.frames).toEqual([
|
|
62
|
-
{
|
|
63
|
-
id: 3,
|
|
64
|
-
type: 0,
|
|
65
|
-
body: "hello",
|
|
66
|
-
},
|
|
67
|
-
]);
|
|
68
|
-
expect(decoded.rest).toHaveLength(0);
|
|
69
|
-
});
|
|
70
|
-
|
|
71
|
-
test("reads several frames out of one buffer", () => {
|
|
72
|
-
const decoded = decodeSourceRconFrames(
|
|
73
|
-
concatBytes(
|
|
74
|
-
encodeSourceRconFrame({
|
|
75
|
-
id: 1,
|
|
76
|
-
type: SourceRconResponse.Value,
|
|
77
|
-
body: "a",
|
|
78
|
-
}),
|
|
79
|
-
encodeSourceRconFrame({
|
|
80
|
-
id: 2,
|
|
81
|
-
type: SourceRconResponse.Auth,
|
|
82
|
-
body: "",
|
|
83
|
-
}),
|
|
84
|
-
),
|
|
85
|
-
);
|
|
86
|
-
|
|
87
|
-
expect(decoded.frames.map((frame) => frame.id)).toEqual([
|
|
88
|
-
1,
|
|
89
|
-
2,
|
|
90
|
-
]);
|
|
91
|
-
});
|
|
92
|
-
|
|
93
|
-
test("keeps a partial frame for the next chunk", () => {
|
|
94
|
-
const packet = encodeSourceRconFrame({
|
|
95
|
-
id: 9,
|
|
96
|
-
type: SourceRconResponse.Value,
|
|
97
|
-
body: "split",
|
|
98
|
-
});
|
|
99
|
-
|
|
100
|
-
const first = decodeSourceRconFrames(packet.subarray(0, 8));
|
|
101
|
-
|
|
102
|
-
expect(first.frames).toEqual([]);
|
|
103
|
-
expect(first.rest).toHaveLength(8);
|
|
104
|
-
|
|
105
|
-
const second = decodeSourceRconFrames(concatBytes(first.rest, packet.subarray(8)));
|
|
106
|
-
|
|
107
|
-
expect(second.frames).toEqual([
|
|
108
|
-
{
|
|
109
|
-
id: 9,
|
|
110
|
-
type: 0,
|
|
111
|
-
body: "split",
|
|
112
|
-
},
|
|
113
|
-
]);
|
|
114
|
-
});
|
|
115
|
-
|
|
116
|
-
test("reads an empty body", () => {
|
|
117
|
-
expect(
|
|
118
|
-
decodeSourceRconFrames(
|
|
119
|
-
encodeSourceRconFrame({
|
|
120
|
-
id: -1,
|
|
121
|
-
type: SourceRconResponse.Auth,
|
|
122
|
-
body: "",
|
|
123
|
-
}),
|
|
124
|
-
).frames,
|
|
125
|
-
).toEqual([
|
|
126
|
-
{
|
|
127
|
-
id: -1,
|
|
128
|
-
type: 2,
|
|
129
|
-
body: "",
|
|
130
|
-
},
|
|
131
|
-
]);
|
|
132
|
-
});
|
|
133
|
-
|
|
134
|
-
test("rejects a packet declaring an impossible size", () => {
|
|
135
|
-
const packet = new Uint8Array(14);
|
|
136
|
-
|
|
137
|
-
new DataView(packet.buffer).setInt32(0, 2, true);
|
|
138
|
-
|
|
139
|
-
const error = (() => {
|
|
140
|
-
try {
|
|
141
|
-
decodeSourceRconFrames(packet);
|
|
142
|
-
|
|
143
|
-
return null;
|
|
144
|
-
} catch (reason: unknown) {
|
|
145
|
-
return reason;
|
|
146
|
-
}
|
|
147
|
-
})();
|
|
148
|
-
|
|
149
|
-
expect(error).toBeInstanceOf(BridgeRconError);
|
|
150
|
-
expect((error as BridgeRconError).code).toBe(BridgeRconCode.Malformed);
|
|
151
|
-
});
|
|
152
|
-
});
|