@serverkgg/bridge 0.0.11 → 0.0.13
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 +11 -1
- package/src/blog/articleFrontmatter.test.ts +95 -0
- package/src/blog/articleFrontmatter.ts +99 -0
- package/src/blog/compile.test.ts +148 -0
- package/src/blog/compile.ts +172 -0
- package/src/blog/compiled.ts +22 -0
- package/src/blog/index.ts +3 -0
- package/src/catalogs/curseforge.test.ts +580 -0
- package/src/catalogs/curseforge.ts +461 -0
- package/src/catalogs/index.ts +2 -0
- package/src/catalogs/thunderstore.test.ts +94 -0
- package/src/catalogs/thunderstore.ts +239 -0
- package/src/cli/validate.ts +7 -0
- package/src/codecs/index.ts +3 -0
- package/src/codecs/ini.test.ts +152 -0
- package/src/codecs/ini.ts +158 -0
- package/src/codecs/sourceCfg.test.ts +100 -0
- package/src/codecs/sourceCfg.ts +111 -0
- package/src/codecs/xmlProperties.test.ts +104 -0
- package/src/codecs/xmlProperties.ts +133 -0
- package/src/context/bridgeContext.ts +40 -0
- package/src/core/bridgeRconError.ts +19 -0
- package/src/core/index.ts +1 -0
- package/src/guides/compile.ts +70 -54
- package/src/install/index.ts +1 -0
- package/src/install/installStamp.test.ts +125 -0
- package/src/install/installStamp.ts +40 -0
- package/src/main.ts +1 -0
- package/src/manifest/index.ts +1 -0
- package/src/manifest/manifest.test.ts +460 -2
- package/src/manifest/manifest.ts +141 -13
- package/src/manifest/primitives.ts +6 -0
- package/src/manifest/runtime.ts +89 -0
- package/src/net/index.ts +2 -0
- package/src/net/loopback.test.ts +152 -0
- package/src/net/loopback.ts +109 -0
- package/src/net/loopbackMethod.ts +7 -0
- package/src/presence/index.ts +1 -0
- package/src/presence/rosterSync.test.ts +179 -0
- package/src/presence/rosterSync.ts +57 -0
- package/src/probes/index.ts +9 -0
- package/src/probes/minecraftPing.test.ts +344 -0
- package/src/probes/minecraftPing.ts +283 -0
- package/src/probes/raknetPing.test.ts +197 -0
- package/src/probes/raknetPing.ts +228 -0
- package/src/protocol/bridgeEvents.ts +4 -0
- package/src/rcon/index.ts +3 -0
- package/src/rcon/sourceRcon.test.ts +308 -0
- package/src/rcon/sourceRcon.ts +276 -0
- package/src/rcon/sourceRconFrame.test.ts +152 -0
- package/src/rcon/sourceRconFrame.ts +99 -0
- package/src/runtime/bridgeCodec.ts +45 -2
- package/src/runtime/bridgeContext.ts +13 -6
- package/src/runtime/bridgeDriver.test.ts +1 -0
- package/src/runtime/bridgeLiveConfig.test.ts +149 -0
- package/src/runtime/bridgeLiveConfig.ts +63 -0
- package/src/runtime/bridgeProbe.ts +13 -1
- package/src/runtime/bridgeRcon.ts +15 -0
- package/src/runtime/bridgeRegistry.ts +1 -0
- package/src/steam/index.ts +7 -0
- package/src/steam/steamApp.test.ts +78 -0
- package/src/steam/steamApp.ts +35 -0
- package/src/steam/steamcmd.test.ts +157 -0
- package/src/steam/steamcmd.ts +273 -0
- package/src/steam/steamcmdLogin.ts +34 -0
- package/src/steam/steamcmdNarrator.test.ts +90 -0
- package/src/steam/steamcmdNarrator.ts +43 -0
- package/src/steam/steamcmdPlatform.ts +4 -0
- package/src/steam/webApi.ts +70 -0
- package/src/steam/workshop.test.ts +45 -0
- package/src/steam/workshop.ts +148 -0
- package/src/utils/execDetail.ts +16 -0
- package/src/utils/index.ts +2 -0
- package/src/utils/token.test.ts +50 -0
- package/src/utils/token.ts +35 -0
- package/src/wine/index.ts +3 -0
- package/src/wine/protonCommand.test.ts +67 -0
- package/src/wine/protonCommand.ts +49 -0
- package/src/wine/wineCommand.test.ts +92 -0
- package/src/wine/wineCommand.ts +54 -0
- package/src/wine/winePrefix.test.ts +111 -0
- package/src/wine/winePrefix.ts +88 -0
- package/src/runtime/bridgeSecrets.test.ts +0 -121
- package/src/runtime/bridgeSecrets.ts +0 -43
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
import { beforeEach, describe, expect, test } from "bun:test";
|
|
2
|
+
import type { BridgeContext } from "../context";
|
|
3
|
+
import type { BridgeValues } from "../core";
|
|
4
|
+
import { BridgeEventName } from "../protocol";
|
|
5
|
+
import { createRosterSync } from "./rosterSync";
|
|
6
|
+
|
|
7
|
+
interface Player {
|
|
8
|
+
id: string;
|
|
9
|
+
name: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
interface Emitted {
|
|
13
|
+
event: string;
|
|
14
|
+
payload: BridgeValues | undefined;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const emitted: Emitted[] = [];
|
|
18
|
+
|
|
19
|
+
const context = {
|
|
20
|
+
emit(event: string, payload?: BridgeValues) {
|
|
21
|
+
emitted.push({
|
|
22
|
+
event,
|
|
23
|
+
payload,
|
|
24
|
+
});
|
|
25
|
+
},
|
|
26
|
+
} as unknown as BridgeContext;
|
|
27
|
+
|
|
28
|
+
const roster = () => {
|
|
29
|
+
return createRosterSync<Player>({
|
|
30
|
+
id: (player) => player.id,
|
|
31
|
+
presenceOf: (player) => {
|
|
32
|
+
return {
|
|
33
|
+
player: player.name,
|
|
34
|
+
steamId: player.id,
|
|
35
|
+
};
|
|
36
|
+
},
|
|
37
|
+
});
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
beforeEach(() => {
|
|
41
|
+
emitted.length = 0;
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
describe("createRosterSync", () => {
|
|
45
|
+
test("emits a join for every player of the first sample", () => {
|
|
46
|
+
const sync = roster();
|
|
47
|
+
|
|
48
|
+
sync.sync(context, [
|
|
49
|
+
{
|
|
50
|
+
id: "1",
|
|
51
|
+
name: "sara",
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
id: "2",
|
|
55
|
+
name: "omar",
|
|
56
|
+
},
|
|
57
|
+
]);
|
|
58
|
+
|
|
59
|
+
expect(emitted).toEqual([
|
|
60
|
+
{
|
|
61
|
+
event: BridgeEventName.PlayerJoined,
|
|
62
|
+
payload: {
|
|
63
|
+
player: "sara",
|
|
64
|
+
steamId: "1",
|
|
65
|
+
},
|
|
66
|
+
},
|
|
67
|
+
{
|
|
68
|
+
event: BridgeEventName.PlayerJoined,
|
|
69
|
+
payload: {
|
|
70
|
+
player: "omar",
|
|
71
|
+
steamId: "2",
|
|
72
|
+
},
|
|
73
|
+
},
|
|
74
|
+
]);
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
test("stays quiet while the roster does not change", () => {
|
|
78
|
+
const sync = roster();
|
|
79
|
+
const rows = [
|
|
80
|
+
{
|
|
81
|
+
id: "1",
|
|
82
|
+
name: "sara",
|
|
83
|
+
},
|
|
84
|
+
];
|
|
85
|
+
|
|
86
|
+
sync.sync(context, rows);
|
|
87
|
+
emitted.length = 0;
|
|
88
|
+
sync.sync(context, rows);
|
|
89
|
+
|
|
90
|
+
expect(emitted).toEqual([]);
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
test("emits a leave carrying the presence the player had", () => {
|
|
94
|
+
const sync = roster();
|
|
95
|
+
|
|
96
|
+
sync.sync(context, [
|
|
97
|
+
{
|
|
98
|
+
id: "1",
|
|
99
|
+
name: "sara",
|
|
100
|
+
},
|
|
101
|
+
]);
|
|
102
|
+
emitted.length = 0;
|
|
103
|
+
sync.sync(context, []);
|
|
104
|
+
|
|
105
|
+
expect(emitted).toEqual([
|
|
106
|
+
{
|
|
107
|
+
event: BridgeEventName.PlayerLeft,
|
|
108
|
+
payload: {
|
|
109
|
+
player: "sara",
|
|
110
|
+
steamId: "1",
|
|
111
|
+
},
|
|
112
|
+
},
|
|
113
|
+
]);
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
test("emits both sides of a swap in one sample", () => {
|
|
117
|
+
const sync = roster();
|
|
118
|
+
|
|
119
|
+
sync.sync(context, [
|
|
120
|
+
{
|
|
121
|
+
id: "1",
|
|
122
|
+
name: "sara",
|
|
123
|
+
},
|
|
124
|
+
]);
|
|
125
|
+
emitted.length = 0;
|
|
126
|
+
sync.sync(context, [
|
|
127
|
+
{
|
|
128
|
+
id: "2",
|
|
129
|
+
name: "omar",
|
|
130
|
+
},
|
|
131
|
+
]);
|
|
132
|
+
|
|
133
|
+
expect(emitted.map((entry) => entry.event)).toEqual([
|
|
134
|
+
BridgeEventName.PlayerJoined,
|
|
135
|
+
BridgeEventName.PlayerLeft,
|
|
136
|
+
]);
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
test("reports who it believes is online", () => {
|
|
140
|
+
const sync = roster();
|
|
141
|
+
|
|
142
|
+
sync.sync(context, [
|
|
143
|
+
{
|
|
144
|
+
id: "1",
|
|
145
|
+
name: "sara",
|
|
146
|
+
},
|
|
147
|
+
]);
|
|
148
|
+
|
|
149
|
+
expect(sync.online()).toEqual([
|
|
150
|
+
{
|
|
151
|
+
id: "1",
|
|
152
|
+
name: "sara",
|
|
153
|
+
},
|
|
154
|
+
]);
|
|
155
|
+
|
|
156
|
+
sync.clear();
|
|
157
|
+
|
|
158
|
+
expect(sync.online()).toEqual([]);
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
test("emits a fresh join after a clear", () => {
|
|
162
|
+
const sync = roster();
|
|
163
|
+
const rows = [
|
|
164
|
+
{
|
|
165
|
+
id: "1",
|
|
166
|
+
name: "sara",
|
|
167
|
+
},
|
|
168
|
+
];
|
|
169
|
+
|
|
170
|
+
sync.sync(context, rows);
|
|
171
|
+
sync.clear();
|
|
172
|
+
emitted.length = 0;
|
|
173
|
+
sync.sync(context, rows);
|
|
174
|
+
|
|
175
|
+
expect(emitted.map((entry) => entry.event)).toEqual([
|
|
176
|
+
BridgeEventName.PlayerJoined,
|
|
177
|
+
]);
|
|
178
|
+
});
|
|
179
|
+
});
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import type { BridgeContext } from "../context";
|
|
2
|
+
import type { BridgeValues } from "../core";
|
|
3
|
+
import { BridgeEventName } from "../protocol";
|
|
4
|
+
|
|
5
|
+
export interface RosterSyncOptions<Row> {
|
|
6
|
+
id(row: Row): string;
|
|
7
|
+
presenceOf(row: Row): BridgeValues;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export interface RosterSync<Row> {
|
|
11
|
+
sync(context: BridgeContext, rows: Row[]): void;
|
|
12
|
+
clear(): void;
|
|
13
|
+
online(): Row[];
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export const createRosterSync = <Row>(options: RosterSyncOptions<Row>): RosterSync<Row> => {
|
|
17
|
+
const present = new Map<string, Row>();
|
|
18
|
+
|
|
19
|
+
return {
|
|
20
|
+
sync(context, rows) {
|
|
21
|
+
const current = new Map(
|
|
22
|
+
rows.map((row) => [
|
|
23
|
+
options.id(row),
|
|
24
|
+
row,
|
|
25
|
+
]),
|
|
26
|
+
);
|
|
27
|
+
|
|
28
|
+
for (const [id, row] of current) {
|
|
29
|
+
if (!present.has(id)) {
|
|
30
|
+
context.emit(BridgeEventName.PlayerJoined, options.presenceOf(row));
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
for (const [id, row] of present) {
|
|
35
|
+
if (!current.has(id)) {
|
|
36
|
+
context.emit(BridgeEventName.PlayerLeft, options.presenceOf(row));
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
present.clear();
|
|
41
|
+
|
|
42
|
+
for (const [id, row] of current) {
|
|
43
|
+
present.set(id, row);
|
|
44
|
+
}
|
|
45
|
+
},
|
|
46
|
+
|
|
47
|
+
clear() {
|
|
48
|
+
present.clear();
|
|
49
|
+
},
|
|
50
|
+
|
|
51
|
+
online() {
|
|
52
|
+
return [
|
|
53
|
+
...present.values(),
|
|
54
|
+
];
|
|
55
|
+
},
|
|
56
|
+
};
|
|
57
|
+
};
|
package/src/probes/index.ts
CHANGED
|
@@ -1 +1,10 @@
|
|
|
1
|
+
export type {
|
|
2
|
+
BridgeA2sInfo,
|
|
3
|
+
BridgeMinecraftPingStatus,
|
|
4
|
+
BridgePingPlayers,
|
|
5
|
+
BridgeRaknetPingStatus,
|
|
6
|
+
} from "../context";
|
|
7
|
+
export { BridgeProbeCode, BridgeProbeError } from "../core";
|
|
1
8
|
export * from "./a2s";
|
|
9
|
+
export * from "./minecraftPing";
|
|
10
|
+
export * from "./raknetPing";
|
|
@@ -0,0 +1,344 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import { BridgeProbeCode, BridgeProbeError } from "../core";
|
|
3
|
+
import { parseStatus, probeMinecraftPing } from "./minecraftPing";
|
|
4
|
+
|
|
5
|
+
const HOST = "127.0.0.1";
|
|
6
|
+
|
|
7
|
+
const CLOSED_PORT = 59_441;
|
|
8
|
+
|
|
9
|
+
const varInt = (value: number) => {
|
|
10
|
+
const bytes: number[] = [];
|
|
11
|
+
|
|
12
|
+
let current = value >>> 0;
|
|
13
|
+
|
|
14
|
+
while (true) {
|
|
15
|
+
if ((current & ~0x7f) === 0) {
|
|
16
|
+
bytes.push(current);
|
|
17
|
+
|
|
18
|
+
return bytes;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
bytes.push((current & 0x7f) | 0x80);
|
|
22
|
+
|
|
23
|
+
current >>>= 7;
|
|
24
|
+
}
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
const readVarInt = (view: Uint8Array, offset: number) => {
|
|
28
|
+
let value = 0;
|
|
29
|
+
let size = 0;
|
|
30
|
+
|
|
31
|
+
while (size < 5) {
|
|
32
|
+
const byte = view[offset + size];
|
|
33
|
+
|
|
34
|
+
if (byte === undefined) {
|
|
35
|
+
throw new Error("varint is truncated");
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
value |= (byte & 0x7f) << (7 * size);
|
|
39
|
+
size += 1;
|
|
40
|
+
|
|
41
|
+
if ((byte & 0x80) === 0) {
|
|
42
|
+
return {
|
|
43
|
+
value,
|
|
44
|
+
size,
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
throw new Error("varint is malformed");
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
const response = (payload: unknown, packetId = 0x00) => {
|
|
53
|
+
const json = new TextEncoder().encode(JSON.stringify(payload));
|
|
54
|
+
|
|
55
|
+
const body = [
|
|
56
|
+
...varInt(packetId),
|
|
57
|
+
...varInt(json.length),
|
|
58
|
+
...json,
|
|
59
|
+
];
|
|
60
|
+
|
|
61
|
+
return new Uint8Array([
|
|
62
|
+
...varInt(body.length),
|
|
63
|
+
...body,
|
|
64
|
+
]);
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
const status = {
|
|
68
|
+
players: {
|
|
69
|
+
online: 3,
|
|
70
|
+
max: 20,
|
|
71
|
+
},
|
|
72
|
+
version: {
|
|
73
|
+
name: "1.21.11",
|
|
74
|
+
},
|
|
75
|
+
description: "A Serverk Minecraft Server",
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
const handshakeServerName = (frame: Uint8Array) => {
|
|
79
|
+
const length = readVarInt(frame, 0);
|
|
80
|
+
const packetId = readVarInt(frame, length.size);
|
|
81
|
+
const protocol = readVarInt(frame, length.size + packetId.size);
|
|
82
|
+
const address = readVarInt(frame, length.size + packetId.size + protocol.size);
|
|
83
|
+
const start = length.size + packetId.size + protocol.size + address.size;
|
|
84
|
+
|
|
85
|
+
return new TextDecoder().decode(frame.subarray(start, start + address.value));
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
const listen = async (reply: Uint8Array | null, onServerName?: (serverName: string) => void) => {
|
|
89
|
+
return Bun.listen({
|
|
90
|
+
hostname: HOST,
|
|
91
|
+
port: 0,
|
|
92
|
+
socket: {
|
|
93
|
+
data(socket, data) {
|
|
94
|
+
if (onServerName) {
|
|
95
|
+
onServerName(handshakeServerName(data));
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
if (reply) {
|
|
99
|
+
socket.write(reply);
|
|
100
|
+
}
|
|
101
|
+
},
|
|
102
|
+
error() {},
|
|
103
|
+
},
|
|
104
|
+
});
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
describe("reading a status response off the wire", () => {
|
|
108
|
+
test("a complete response gives the player counts, the version and the motd", () => {
|
|
109
|
+
expect(parseStatus(response(status))).toEqual({
|
|
110
|
+
players: {
|
|
111
|
+
online: 3,
|
|
112
|
+
max: 20,
|
|
113
|
+
},
|
|
114
|
+
version: "1.21.11",
|
|
115
|
+
description: "A Serverk Minecraft Server",
|
|
116
|
+
});
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
test("a response longer than one packet is still read, because the frame carries its own length", () => {
|
|
120
|
+
const packet = response(status);
|
|
121
|
+
const padded = new Uint8Array(packet.length + 4);
|
|
122
|
+
|
|
123
|
+
padded.set(packet);
|
|
124
|
+
|
|
125
|
+
expect(parseStatus(padded)?.players).toEqual({
|
|
126
|
+
online: 3,
|
|
127
|
+
max: 20,
|
|
128
|
+
});
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
test("a modern description object is read through its text", () => {
|
|
132
|
+
expect(
|
|
133
|
+
parseStatus(
|
|
134
|
+
response({
|
|
135
|
+
...status,
|
|
136
|
+
description: {
|
|
137
|
+
text: "hello",
|
|
138
|
+
},
|
|
139
|
+
}),
|
|
140
|
+
)?.description,
|
|
141
|
+
).toBe("hello");
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
test("a description we cannot read is dropped rather than failing the sample", () => {
|
|
145
|
+
expect(
|
|
146
|
+
parseStatus(
|
|
147
|
+
response({
|
|
148
|
+
...status,
|
|
149
|
+
description: {
|
|
150
|
+
extra: [],
|
|
151
|
+
},
|
|
152
|
+
}),
|
|
153
|
+
)?.description,
|
|
154
|
+
).toBeNull();
|
|
155
|
+
expect(
|
|
156
|
+
parseStatus(
|
|
157
|
+
response({
|
|
158
|
+
...status,
|
|
159
|
+
description: null,
|
|
160
|
+
}),
|
|
161
|
+
)?.description,
|
|
162
|
+
).toBeNull();
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
test("a missing version name is dropped rather than failing the sample", () => {
|
|
166
|
+
expect(
|
|
167
|
+
parseStatus(
|
|
168
|
+
response({
|
|
169
|
+
players: status.players,
|
|
170
|
+
}),
|
|
171
|
+
)?.version,
|
|
172
|
+
).toBeNull();
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
test("fractional or negative counts are clamped to whole players", () => {
|
|
176
|
+
expect(
|
|
177
|
+
parseStatus(
|
|
178
|
+
response({
|
|
179
|
+
players: {
|
|
180
|
+
online: -1,
|
|
181
|
+
max: 20.7,
|
|
182
|
+
},
|
|
183
|
+
}),
|
|
184
|
+
)?.players,
|
|
185
|
+
).toEqual({
|
|
186
|
+
online: 0,
|
|
187
|
+
max: 20,
|
|
188
|
+
});
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
test("a response we have not fully received yet asks for more bytes", () => {
|
|
192
|
+
const packet = response(status);
|
|
193
|
+
|
|
194
|
+
expect(parseStatus(new Uint8Array(0))).toBeNull();
|
|
195
|
+
expect(parseStatus(packet.subarray(0, 1))).toBeNull();
|
|
196
|
+
expect(parseStatus(packet.subarray(0, packet.length - 4))).toBeNull();
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
test("a large payload whose length needs two varint bytes is read whole", () => {
|
|
200
|
+
const long = {
|
|
201
|
+
players: status.players,
|
|
202
|
+
description: "n".repeat(400),
|
|
203
|
+
};
|
|
204
|
+
|
|
205
|
+
expect(parseStatus(response(long))?.description).toBe(long.description);
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
test("a packet that is not a status response is refused", () => {
|
|
209
|
+
expect(() => parseStatus(response(status, 0x01))).toThrow();
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
test("a response with no player counts is refused, because a sample without them is a lie", () => {
|
|
213
|
+
expect(() =>
|
|
214
|
+
parseStatus(
|
|
215
|
+
response({
|
|
216
|
+
version: {
|
|
217
|
+
name: "1.21.11",
|
|
218
|
+
},
|
|
219
|
+
}),
|
|
220
|
+
),
|
|
221
|
+
).toThrow();
|
|
222
|
+
});
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
describe("probeMinecraftPing", () => {
|
|
226
|
+
test("answers with the status a listening server sends back", async () => {
|
|
227
|
+
const server = await listen(response(status));
|
|
228
|
+
|
|
229
|
+
try {
|
|
230
|
+
const sample = await probeMinecraftPing(
|
|
231
|
+
{
|
|
232
|
+
address: HOST,
|
|
233
|
+
port: server.port,
|
|
234
|
+
},
|
|
235
|
+
2000,
|
|
236
|
+
);
|
|
237
|
+
|
|
238
|
+
expect(sample.players).toEqual({
|
|
239
|
+
online: 3,
|
|
240
|
+
max: 20,
|
|
241
|
+
});
|
|
242
|
+
expect(sample.version).toBe("1.21.11");
|
|
243
|
+
expect(sample.latencyMs).toBeGreaterThanOrEqual(0);
|
|
244
|
+
} finally {
|
|
245
|
+
server.stop(true);
|
|
246
|
+
}
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
test("puts the server name in the handshake so a virtual host still answers", async () => {
|
|
250
|
+
const seen: string[] = [];
|
|
251
|
+
|
|
252
|
+
const server = await listen(response(status), (serverName) => {
|
|
253
|
+
seen.push(serverName);
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
try {
|
|
257
|
+
await probeMinecraftPing(
|
|
258
|
+
{
|
|
259
|
+
address: HOST,
|
|
260
|
+
port: server.port,
|
|
261
|
+
serverName: "play.example.com",
|
|
262
|
+
},
|
|
263
|
+
2000,
|
|
264
|
+
);
|
|
265
|
+
|
|
266
|
+
expect(seen.at(0)).toBe("play.example.com");
|
|
267
|
+
} finally {
|
|
268
|
+
server.stop(true);
|
|
269
|
+
}
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
test("falls back to the address when no server name is given", async () => {
|
|
273
|
+
const seen: string[] = [];
|
|
274
|
+
|
|
275
|
+
const server = await listen(response(status), (serverName) => {
|
|
276
|
+
seen.push(serverName);
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
try {
|
|
280
|
+
await probeMinecraftPing(
|
|
281
|
+
{
|
|
282
|
+
address: HOST,
|
|
283
|
+
port: server.port,
|
|
284
|
+
},
|
|
285
|
+
2000,
|
|
286
|
+
);
|
|
287
|
+
|
|
288
|
+
expect(seen.at(0)).toBe(HOST);
|
|
289
|
+
} finally {
|
|
290
|
+
server.stop(true);
|
|
291
|
+
}
|
|
292
|
+
});
|
|
293
|
+
|
|
294
|
+
test("rejects as unreachable when nothing listens on the port", async () => {
|
|
295
|
+
const error = await probeMinecraftPing(
|
|
296
|
+
{
|
|
297
|
+
address: HOST,
|
|
298
|
+
port: CLOSED_PORT,
|
|
299
|
+
},
|
|
300
|
+
2000,
|
|
301
|
+
).catch((reason: unknown) => reason);
|
|
302
|
+
|
|
303
|
+
expect(error).toBeInstanceOf(BridgeProbeError);
|
|
304
|
+
expect((error as BridgeProbeError).code).toBe(BridgeProbeCode.Unreachable);
|
|
305
|
+
});
|
|
306
|
+
|
|
307
|
+
test("times out against a port that never answers", async () => {
|
|
308
|
+
const silent = await listen(null);
|
|
309
|
+
|
|
310
|
+
try {
|
|
311
|
+
const error = await probeMinecraftPing(
|
|
312
|
+
{
|
|
313
|
+
address: HOST,
|
|
314
|
+
port: silent.port,
|
|
315
|
+
},
|
|
316
|
+
250,
|
|
317
|
+
).catch((reason: unknown) => reason);
|
|
318
|
+
|
|
319
|
+
expect(error).toBeInstanceOf(BridgeProbeError);
|
|
320
|
+
expect((error as BridgeProbeError).code).toBe(BridgeProbeCode.TimedOut);
|
|
321
|
+
} finally {
|
|
322
|
+
silent.stop(true);
|
|
323
|
+
}
|
|
324
|
+
});
|
|
325
|
+
|
|
326
|
+
test("rejects a reply that is not a status response", async () => {
|
|
327
|
+
const noisy = await listen(response(status, 0x01));
|
|
328
|
+
|
|
329
|
+
try {
|
|
330
|
+
const error = await probeMinecraftPing(
|
|
331
|
+
{
|
|
332
|
+
address: HOST,
|
|
333
|
+
port: noisy.port,
|
|
334
|
+
},
|
|
335
|
+
2000,
|
|
336
|
+
).catch((reason: unknown) => reason);
|
|
337
|
+
|
|
338
|
+
expect(error).toBeInstanceOf(BridgeProbeError);
|
|
339
|
+
expect((error as BridgeProbeError).code).toBe(BridgeProbeCode.Malformed);
|
|
340
|
+
} finally {
|
|
341
|
+
noisy.stop(true);
|
|
342
|
+
}
|
|
343
|
+
});
|
|
344
|
+
});
|