@hytaleone/query 1.0.1 → 1.1.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/README.md +110 -2
- package/dist/index.cjs +402 -11
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +101 -1
- package/dist/index.d.ts +101 -1
- package/dist/index.js +396 -10
- package/dist/index.js.map +1 -1
- package/package.json +12 -4
package/README.md
CHANGED
|
@@ -1,6 +1,19 @@
|
|
|
1
1
|
# @hytaleone/query
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
[](https://www.npmjs.com/package/@hytaleone/query)
|
|
4
|
+
[](https://opensource.org/licenses/MIT)
|
|
5
|
+
|
|
6
|
+
Query Hytale servers using the UDP query protocol. Get server status, player count, player list, and plugin information.
|
|
7
|
+
|
|
8
|
+
## Features
|
|
9
|
+
|
|
10
|
+
- **Server Status** - Get server name, MOTD, version, player count
|
|
11
|
+
- **Player List** - Retrieve online players with names and UUIDs
|
|
12
|
+
- **Plugin List** - See installed plugins with versions
|
|
13
|
+
- **V2 Protocol** - Enhanced protocol with challenge-based authentication and pagination
|
|
14
|
+
- **Zero dependencies** - Uses only Node.js built-in modules
|
|
15
|
+
- **TypeScript** - Full type definitions included
|
|
16
|
+
- **Dual format** - Works with both ESM and CommonJS
|
|
4
17
|
|
|
5
18
|
## Installation
|
|
6
19
|
|
|
@@ -10,6 +23,8 @@ npm install @hytaleone/query
|
|
|
10
23
|
|
|
11
24
|
## Usage
|
|
12
25
|
|
|
26
|
+
### V1 Protocol
|
|
27
|
+
|
|
13
28
|
```typescript
|
|
14
29
|
import { query } from '@hytaleone/query';
|
|
15
30
|
|
|
@@ -17,17 +32,46 @@ import { query } from '@hytaleone/query';
|
|
|
17
32
|
const info = await query('play.example.com', 5520);
|
|
18
33
|
console.log(`${info.serverName}: ${info.currentPlayers}/${info.maxPlayers}`);
|
|
19
34
|
|
|
35
|
+
// Check V2 support
|
|
36
|
+
if (info.supportsV2) {
|
|
37
|
+
console.log('Server supports V2 protocol');
|
|
38
|
+
}
|
|
39
|
+
|
|
20
40
|
// Full query - includes players and plugins
|
|
21
41
|
const full = await query('play.example.com', 5520, { full: true });
|
|
22
42
|
console.log('Players:', full.players.map(p => p.name).join(', '));
|
|
23
43
|
console.log('Plugins:', full.plugins.map(p => p.id).join(', '));
|
|
24
44
|
```
|
|
25
45
|
|
|
46
|
+
### V2 Protocol
|
|
47
|
+
|
|
48
|
+
```typescript
|
|
49
|
+
import { query, queryV2 } from '@hytaleone/query';
|
|
50
|
+
|
|
51
|
+
// Check if server supports V2
|
|
52
|
+
const info = await query('play.example.com', 5520);
|
|
53
|
+
|
|
54
|
+
if (info.supportsV2) {
|
|
55
|
+
// Basic V2 query
|
|
56
|
+
const v2Info = await queryV2('play.example.com', 5520);
|
|
57
|
+
console.log(`${v2Info.serverName}: ${v2Info.currentPlayers}/${v2Info.maxPlayers}`);
|
|
58
|
+
|
|
59
|
+
// V2 query with players (single page)
|
|
60
|
+
const withPlayers = await queryV2('play.example.com', 5520, { players: true });
|
|
61
|
+
console.log('Players:', withPlayers.players.map(p => p.name).join(', '));
|
|
62
|
+
console.log('Has more:', withPlayers.hasMore);
|
|
63
|
+
|
|
64
|
+
// V2 query with all players (auto-pagination)
|
|
65
|
+
const allPlayers = await queryV2('play.example.com', 5520, { players: 'all' });
|
|
66
|
+
console.log(`All ${allPlayers.totalPlayers} players fetched`);
|
|
67
|
+
}
|
|
68
|
+
```
|
|
69
|
+
|
|
26
70
|
## API
|
|
27
71
|
|
|
28
72
|
### query(host, port?, options?)
|
|
29
73
|
|
|
30
|
-
Query a server
|
|
74
|
+
Query a server using the V1 protocol.
|
|
31
75
|
|
|
32
76
|
```typescript
|
|
33
77
|
const info = await query('localhost', 5520, {
|
|
@@ -49,11 +93,71 @@ const info = await query('localhost', 5520, {
|
|
|
49
93
|
- `version` - Server version
|
|
50
94
|
- `protocolVersion` - Protocol version number
|
|
51
95
|
- `protocolHash` - Protocol hash
|
|
96
|
+
- `supportsV2` - Whether server supports V2 protocol
|
|
97
|
+
- `isNetworkMode` - Whether server is in network aggregation mode
|
|
98
|
+
- `v2Version` - V2 protocol version (0 if not supported)
|
|
52
99
|
|
|
53
100
|
**With `full: true`, also returns:**
|
|
54
101
|
- `players` - Array of `{ name, uuid }`
|
|
55
102
|
- `plugins` - Array of `{ id, version, enabled }`
|
|
56
103
|
|
|
104
|
+
### queryV2(host, port?, options?)
|
|
105
|
+
|
|
106
|
+
Query a server using the V2 protocol with challenge-based authentication.
|
|
107
|
+
|
|
108
|
+
```typescript
|
|
109
|
+
const info = await queryV2('localhost', 5520, {
|
|
110
|
+
timeout: 5000,
|
|
111
|
+
players: true,
|
|
112
|
+
playerOffset: 0
|
|
113
|
+
});
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
**Options:**
|
|
117
|
+
- `timeout` - Query timeout in milliseconds (default: 5000)
|
|
118
|
+
- `players` - Request player list: `false` (default), `true` (single page), or `'all'` (auto-paginate)
|
|
119
|
+
- `playerOffset` - Starting offset for player pagination (default: 0)
|
|
120
|
+
- `authToken` - Optional authentication token for private servers
|
|
121
|
+
|
|
122
|
+
**Returns `ServerInfoV2`:**
|
|
123
|
+
- `serverName` - Server display name
|
|
124
|
+
- `motd` - Message of the day
|
|
125
|
+
- `currentPlayers` - Current player count
|
|
126
|
+
- `maxPlayers` - Maximum player capacity
|
|
127
|
+
- `version` - Server version
|
|
128
|
+
- `protocolVersion` - Protocol version number
|
|
129
|
+
- `protocolHash` - Protocol hash
|
|
130
|
+
- `isNetwork` - Whether response contains aggregated network data
|
|
131
|
+
- `host` - Server host (if provided by server)
|
|
132
|
+
- `hostPort` - Server port (if provided by server)
|
|
133
|
+
|
|
134
|
+
**With `players: true` or `players: 'all'`, also returns:**
|
|
135
|
+
- `players` - Array of `{ name, uuid }`
|
|
136
|
+
- `totalPlayers` - Total player count on server
|
|
137
|
+
- `offset` - Offset used for this response
|
|
138
|
+
- `hasMore` - Whether more players are available
|
|
139
|
+
|
|
140
|
+
### clearChallengeCache()
|
|
141
|
+
|
|
142
|
+
Clear the V2 challenge token cache. Useful for testing or forcing fresh challenges.
|
|
143
|
+
|
|
144
|
+
```typescript
|
|
145
|
+
import { clearChallengeCache } from '@hytaleone/query';
|
|
146
|
+
|
|
147
|
+
clearChallengeCache();
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
## V1 vs V2 Protocol
|
|
151
|
+
|
|
152
|
+
| Feature | V1 | V2 |
|
|
153
|
+
|---------|----|----|
|
|
154
|
+
| Authentication | None | Challenge-based |
|
|
155
|
+
| Player pagination | No | Yes |
|
|
156
|
+
| Network aggregation | No | Yes |
|
|
157
|
+
| Plugin list | Yes | No |
|
|
158
|
+
|
|
159
|
+
Use V1 for simple queries or when you need plugin information. Use V2 for servers with many players or when you need pagination support.
|
|
160
|
+
|
|
57
161
|
## Requirements
|
|
58
162
|
|
|
59
163
|
- Node.js >= 18
|
|
@@ -63,6 +167,10 @@ const info = await query('localhost', 5520, {
|
|
|
63
167
|
|
|
64
168
|
MIT
|
|
65
169
|
|
|
170
|
+
## Related
|
|
171
|
+
|
|
172
|
+
- [@hytaleone/votifier](https://www.npmjs.com/package/@hytaleone/votifier) - Send votes to Hytale and Minecraft servers
|
|
173
|
+
|
|
66
174
|
---
|
|
67
175
|
|
|
68
176
|
**[hytale.one](https://hytale.one/)** - Discover Hytale Servers
|
package/dist/index.cjs
CHANGED
|
@@ -30,7 +30,12 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
|
|
|
30
30
|
// src/index.ts
|
|
31
31
|
var index_exports = {};
|
|
32
32
|
__export(index_exports, {
|
|
33
|
-
|
|
33
|
+
V2QueryType: () => V2QueryType,
|
|
34
|
+
V2ResponseFlag: () => V2ResponseFlag,
|
|
35
|
+
V2TLVType: () => V2TLVType,
|
|
36
|
+
clearChallengeCache: () => clearChallengeCache,
|
|
37
|
+
query: () => query,
|
|
38
|
+
queryV2: () => queryV2
|
|
34
39
|
});
|
|
35
40
|
module.exports = __toCommonJS(index_exports);
|
|
36
41
|
|
|
@@ -42,6 +47,8 @@ var REQUEST_MAGIC = Buffer.from("HYQUERY\0", "ascii");
|
|
|
42
47
|
var RESPONSE_MAGIC = Buffer.from("HYREPLY\0", "ascii");
|
|
43
48
|
var TYPE_BASIC = 0;
|
|
44
49
|
var TYPE_FULL = 1;
|
|
50
|
+
var CAP_V2_PROTOCOL = 1;
|
|
51
|
+
var CAP_NETWORK_MODE = 2;
|
|
45
52
|
function buildRequest(type) {
|
|
46
53
|
const buf = Buffer.alloc(REQUEST_MAGIC.length + 1);
|
|
47
54
|
REQUEST_MAGIC.copy(buf, 0);
|
|
@@ -53,31 +60,58 @@ var BufferReader = class {
|
|
|
53
60
|
this.buf = buf;
|
|
54
61
|
}
|
|
55
62
|
offset = 0;
|
|
63
|
+
checkBounds(needed) {
|
|
64
|
+
if (this.offset + needed > this.buf.length) {
|
|
65
|
+
throw new RangeError(
|
|
66
|
+
`Buffer underflow: need ${needed} bytes at offset ${this.offset}, but buffer is ${this.buf.length} bytes`
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
56
70
|
readBytes(length) {
|
|
71
|
+
this.checkBounds(length);
|
|
57
72
|
const slice = this.buf.subarray(this.offset, this.offset + length);
|
|
58
73
|
this.offset += length;
|
|
59
74
|
return slice;
|
|
60
75
|
}
|
|
76
|
+
readUInt8() {
|
|
77
|
+
this.checkBounds(1);
|
|
78
|
+
return this.buf[this.offset++];
|
|
79
|
+
}
|
|
61
80
|
readUInt16LE() {
|
|
81
|
+
this.checkBounds(2);
|
|
62
82
|
const value = this.buf.readUInt16LE(this.offset);
|
|
63
83
|
this.offset += 2;
|
|
64
84
|
return value;
|
|
65
85
|
}
|
|
86
|
+
readUInt32LE() {
|
|
87
|
+
this.checkBounds(4);
|
|
88
|
+
const value = this.buf.readUInt32LE(this.offset);
|
|
89
|
+
this.offset += 4;
|
|
90
|
+
return value;
|
|
91
|
+
}
|
|
66
92
|
readInt32LE() {
|
|
93
|
+
this.checkBounds(4);
|
|
67
94
|
const value = this.buf.readInt32LE(this.offset);
|
|
68
95
|
this.offset += 4;
|
|
69
96
|
return value;
|
|
70
97
|
}
|
|
71
98
|
readBigInt64BE() {
|
|
99
|
+
this.checkBounds(8);
|
|
72
100
|
const value = this.buf.readBigInt64BE(this.offset);
|
|
73
101
|
this.offset += 8;
|
|
74
102
|
return value;
|
|
75
103
|
}
|
|
76
104
|
readBoolean() {
|
|
105
|
+
this.checkBounds(1);
|
|
77
106
|
return this.buf[this.offset++] !== 0;
|
|
78
107
|
}
|
|
79
108
|
readString() {
|
|
80
109
|
const length = this.readUInt16LE();
|
|
110
|
+
if (length > this.remaining) {
|
|
111
|
+
throw new RangeError(
|
|
112
|
+
`Invalid string length ${length} at offset ${this.offset - 2}, only ${this.remaining} bytes remaining`
|
|
113
|
+
);
|
|
114
|
+
}
|
|
81
115
|
const bytes = this.readBytes(length);
|
|
82
116
|
return bytes.toString("utf8");
|
|
83
117
|
}
|
|
@@ -86,9 +120,23 @@ var BufferReader = class {
|
|
|
86
120
|
const lsb = this.readBigInt64BE();
|
|
87
121
|
return formatUUID(msb, lsb);
|
|
88
122
|
}
|
|
123
|
+
readBigInt64LE() {
|
|
124
|
+
this.checkBounds(8);
|
|
125
|
+
const value = this.buf.readBigInt64LE(this.offset);
|
|
126
|
+
this.offset += 8;
|
|
127
|
+
return value;
|
|
128
|
+
}
|
|
129
|
+
readUUIDLE() {
|
|
130
|
+
const msb = this.readBigInt64LE();
|
|
131
|
+
const lsb = this.readBigInt64LE();
|
|
132
|
+
return formatUUID(msb, lsb);
|
|
133
|
+
}
|
|
89
134
|
get remaining() {
|
|
90
135
|
return this.buf.length - this.offset;
|
|
91
136
|
}
|
|
137
|
+
get position() {
|
|
138
|
+
return this.offset;
|
|
139
|
+
}
|
|
92
140
|
};
|
|
93
141
|
function formatUUID(msb, lsb) {
|
|
94
142
|
const toHex = (n) => {
|
|
@@ -106,6 +154,18 @@ function validateResponse(buf) {
|
|
|
106
154
|
}
|
|
107
155
|
return buf.subarray(0, RESPONSE_MAGIC.length).equals(RESPONSE_MAGIC);
|
|
108
156
|
}
|
|
157
|
+
function parseCapabilities(reader) {
|
|
158
|
+
if (reader.remaining >= 3) {
|
|
159
|
+
const caps = reader.readUInt16LE();
|
|
160
|
+
const v2Version = reader.readUInt8();
|
|
161
|
+
return {
|
|
162
|
+
supportsV2: (caps & CAP_V2_PROTOCOL) !== 0,
|
|
163
|
+
isNetworkMode: (caps & CAP_NETWORK_MODE) !== 0,
|
|
164
|
+
v2Version
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
return { supportsV2: false, isNetworkMode: false, v2Version: 0 };
|
|
168
|
+
}
|
|
109
169
|
function parseBasicResponse(buf) {
|
|
110
170
|
if (!validateResponse(buf)) {
|
|
111
171
|
throw new Error("Invalid response: magic mismatch");
|
|
@@ -113,15 +173,25 @@ function parseBasicResponse(buf) {
|
|
|
113
173
|
const reader = new BufferReader(buf);
|
|
114
174
|
reader.readBytes(RESPONSE_MAGIC.length);
|
|
115
175
|
reader.readBytes(1);
|
|
176
|
+
const serverName = reader.readString();
|
|
177
|
+
const motd = reader.readString();
|
|
178
|
+
const currentPlayers = reader.readInt32LE();
|
|
179
|
+
const maxPlayers = reader.readInt32LE();
|
|
180
|
+
const hostPort = reader.readUInt16LE();
|
|
181
|
+
const version = reader.readString();
|
|
182
|
+
const protocolVersion = reader.readInt32LE();
|
|
183
|
+
const protocolHash = reader.readString();
|
|
184
|
+
const capabilities = parseCapabilities(reader);
|
|
116
185
|
return {
|
|
117
|
-
serverName
|
|
118
|
-
motd
|
|
119
|
-
currentPlayers
|
|
120
|
-
maxPlayers
|
|
121
|
-
hostPort
|
|
122
|
-
version
|
|
123
|
-
protocolVersion
|
|
124
|
-
protocolHash
|
|
186
|
+
serverName,
|
|
187
|
+
motd,
|
|
188
|
+
currentPlayers,
|
|
189
|
+
maxPlayers,
|
|
190
|
+
hostPort,
|
|
191
|
+
version,
|
|
192
|
+
protocolVersion,
|
|
193
|
+
protocolHash,
|
|
194
|
+
...capabilities
|
|
125
195
|
};
|
|
126
196
|
}
|
|
127
197
|
function parseFullResponse(buf) {
|
|
@@ -156,6 +226,7 @@ function parseFullResponse(buf) {
|
|
|
156
226
|
enabled: reader.readBoolean()
|
|
157
227
|
});
|
|
158
228
|
}
|
|
229
|
+
const capabilities = parseCapabilities(reader);
|
|
159
230
|
return {
|
|
160
231
|
serverName,
|
|
161
232
|
motd,
|
|
@@ -166,7 +237,8 @@ function parseFullResponse(buf) {
|
|
|
166
237
|
protocolVersion,
|
|
167
238
|
protocolHash,
|
|
168
239
|
players,
|
|
169
|
-
plugins
|
|
240
|
+
plugins,
|
|
241
|
+
...capabilities
|
|
170
242
|
};
|
|
171
243
|
}
|
|
172
244
|
|
|
@@ -216,8 +288,327 @@ async function query(host, port = 5520, options = {}) {
|
|
|
216
288
|
}
|
|
217
289
|
return parseBasicResponse(response);
|
|
218
290
|
}
|
|
291
|
+
|
|
292
|
+
// src/query-v2.ts
|
|
293
|
+
var import_node_dgram2 = __toESM(require("dgram"), 1);
|
|
294
|
+
|
|
295
|
+
// src/types-v2.ts
|
|
296
|
+
var V2QueryType = {
|
|
297
|
+
CHALLENGE: 0,
|
|
298
|
+
BASIC: 1,
|
|
299
|
+
PLAYERS: 2
|
|
300
|
+
};
|
|
301
|
+
var V2ResponseFlag = {
|
|
302
|
+
HAS_MORE_PLAYERS: 1,
|
|
303
|
+
AUTH_REQUIRED: 2,
|
|
304
|
+
IS_NETWORK: 16,
|
|
305
|
+
HAS_ADDRESS: 32
|
|
306
|
+
};
|
|
307
|
+
var V2TLVType = {
|
|
308
|
+
SERVER_INFO: 1,
|
|
309
|
+
PLAYER_LIST: 2
|
|
310
|
+
};
|
|
311
|
+
|
|
312
|
+
// src/protocol-v2.ts
|
|
313
|
+
var V2_REQUEST_MAGIC = Buffer.from("ONEQUERY", "ascii");
|
|
314
|
+
var V2_RESPONSE_MAGIC = Buffer.from("ONEREPLY", "ascii");
|
|
315
|
+
var CHALLENGE_TOKEN_SIZE = 32;
|
|
316
|
+
function buildChallengeRequest() {
|
|
317
|
+
const buf = Buffer.alloc(V2_REQUEST_MAGIC.length + 1);
|
|
318
|
+
V2_REQUEST_MAGIC.copy(buf, 0);
|
|
319
|
+
buf[V2_REQUEST_MAGIC.length] = V2QueryType.CHALLENGE;
|
|
320
|
+
return buf;
|
|
321
|
+
}
|
|
322
|
+
function buildV2Request(type, token, requestId, flags, offset, authToken) {
|
|
323
|
+
const authBuf = authToken ? Buffer.from(authToken, "utf8") : null;
|
|
324
|
+
const authLength = authBuf ? 2 + authBuf.length : 0;
|
|
325
|
+
const totalLength = V2_REQUEST_MAGIC.length + 1 + 32 + 4 + 2 + 4 + authLength;
|
|
326
|
+
const buf = Buffer.alloc(totalLength);
|
|
327
|
+
let pos = 0;
|
|
328
|
+
V2_REQUEST_MAGIC.copy(buf, pos);
|
|
329
|
+
pos += V2_REQUEST_MAGIC.length;
|
|
330
|
+
buf[pos++] = type;
|
|
331
|
+
token.copy(buf, pos);
|
|
332
|
+
pos += 32;
|
|
333
|
+
buf.writeUInt32LE(requestId, pos);
|
|
334
|
+
pos += 4;
|
|
335
|
+
buf.writeUInt16LE(flags, pos);
|
|
336
|
+
pos += 2;
|
|
337
|
+
buf.writeUInt32LE(offset, pos);
|
|
338
|
+
pos += 4;
|
|
339
|
+
if (authBuf) {
|
|
340
|
+
buf.writeUInt16LE(authBuf.length, pos);
|
|
341
|
+
pos += 2;
|
|
342
|
+
authBuf.copy(buf, pos);
|
|
343
|
+
}
|
|
344
|
+
return buf;
|
|
345
|
+
}
|
|
346
|
+
function validateV2Response(buf) {
|
|
347
|
+
if (buf.length < V2_RESPONSE_MAGIC.length) {
|
|
348
|
+
return false;
|
|
349
|
+
}
|
|
350
|
+
return buf.subarray(0, V2_RESPONSE_MAGIC.length).equals(V2_RESPONSE_MAGIC);
|
|
351
|
+
}
|
|
352
|
+
function parseChallengeResponse(buf) {
|
|
353
|
+
if (!validateV2Response(buf)) {
|
|
354
|
+
throw new Error("Invalid V2 response: magic mismatch");
|
|
355
|
+
}
|
|
356
|
+
const tokenOffset = V2_RESPONSE_MAGIC.length + 1;
|
|
357
|
+
if (buf.length < tokenOffset + CHALLENGE_TOKEN_SIZE) {
|
|
358
|
+
throw new Error("Invalid challenge response: too short");
|
|
359
|
+
}
|
|
360
|
+
return Buffer.from(buf.subarray(tokenOffset, tokenOffset + CHALLENGE_TOKEN_SIZE));
|
|
361
|
+
}
|
|
362
|
+
function parseV2ResponseHeader(buf) {
|
|
363
|
+
if (!validateV2Response(buf)) {
|
|
364
|
+
throw new Error("Invalid V2 response: magic mismatch");
|
|
365
|
+
}
|
|
366
|
+
const reader = new BufferReader(buf);
|
|
367
|
+
reader.readBytes(V2_RESPONSE_MAGIC.length);
|
|
368
|
+
const protocolVersion = reader.readUInt8();
|
|
369
|
+
const flags = reader.readUInt16LE();
|
|
370
|
+
const requestId = reader.readUInt32LE();
|
|
371
|
+
const payloadLength = reader.readUInt16LE();
|
|
372
|
+
return { protocolVersion, flags, requestId, payloadLength };
|
|
373
|
+
}
|
|
374
|
+
function parseTLVEntries(payload) {
|
|
375
|
+
const entries = [];
|
|
376
|
+
const reader = new BufferReader(payload);
|
|
377
|
+
while (reader.remaining >= 4) {
|
|
378
|
+
const type = reader.readUInt16LE();
|
|
379
|
+
const length = reader.readUInt16LE();
|
|
380
|
+
if (reader.remaining < length) {
|
|
381
|
+
throw new Error("Invalid TLV: truncated value");
|
|
382
|
+
}
|
|
383
|
+
const value = reader.readBytes(length);
|
|
384
|
+
entries.push({ type, value });
|
|
385
|
+
}
|
|
386
|
+
return entries;
|
|
387
|
+
}
|
|
388
|
+
function parseV2Response(buf) {
|
|
389
|
+
const header = parseV2ResponseHeader(buf);
|
|
390
|
+
const headerSize = V2_RESPONSE_MAGIC.length + 1 + 2 + 4 + 2;
|
|
391
|
+
const payload = buf.subarray(headerSize, headerSize + header.payloadLength);
|
|
392
|
+
const entries = parseTLVEntries(payload);
|
|
393
|
+
return { header, entries };
|
|
394
|
+
}
|
|
395
|
+
function parseTLVServerInfo(entry, hasAddress, isNetwork) {
|
|
396
|
+
if (entry.type !== V2TLVType.SERVER_INFO) {
|
|
397
|
+
throw new Error(`Expected SERVER_INFO TLV (${V2TLVType.SERVER_INFO}), got ${entry.type}`);
|
|
398
|
+
}
|
|
399
|
+
const reader = new BufferReader(entry.value);
|
|
400
|
+
const serverName = reader.readString();
|
|
401
|
+
const motd = reader.readString();
|
|
402
|
+
const currentPlayers = reader.readInt32LE();
|
|
403
|
+
const maxPlayers = reader.readInt32LE();
|
|
404
|
+
const version = reader.readString();
|
|
405
|
+
const protocolVersion = reader.readInt32LE();
|
|
406
|
+
const protocolHash = reader.readString();
|
|
407
|
+
const result = {
|
|
408
|
+
serverName,
|
|
409
|
+
motd,
|
|
410
|
+
currentPlayers,
|
|
411
|
+
maxPlayers,
|
|
412
|
+
version,
|
|
413
|
+
protocolVersion,
|
|
414
|
+
protocolHash,
|
|
415
|
+
isNetwork
|
|
416
|
+
};
|
|
417
|
+
if (hasAddress && reader.remaining >= 2) {
|
|
418
|
+
result.host = reader.readString();
|
|
419
|
+
if (reader.remaining >= 2) {
|
|
420
|
+
result.hostPort = reader.readUInt16LE();
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
return result;
|
|
424
|
+
}
|
|
425
|
+
function parseTLVPlayerList(entry) {
|
|
426
|
+
if (entry.type !== V2TLVType.PLAYER_LIST) {
|
|
427
|
+
throw new Error(`Expected PLAYER_LIST TLV (${V2TLVType.PLAYER_LIST}), got ${entry.type}`);
|
|
428
|
+
}
|
|
429
|
+
const reader = new BufferReader(entry.value);
|
|
430
|
+
const totalPlayers = reader.readInt32LE();
|
|
431
|
+
const playersInResponse = reader.readInt32LE();
|
|
432
|
+
const offset = reader.readInt32LE();
|
|
433
|
+
const players = [];
|
|
434
|
+
for (let i = 0; i < playersInResponse; i++) {
|
|
435
|
+
const name = reader.readString();
|
|
436
|
+
const uuid = reader.readUUIDLE();
|
|
437
|
+
players.push({ name, uuid });
|
|
438
|
+
}
|
|
439
|
+
return { players, totalPlayers, offset };
|
|
440
|
+
}
|
|
441
|
+
function findTLVEntry(entries, type) {
|
|
442
|
+
return entries.find((e) => e.type === type);
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
// src/query-v2.ts
|
|
446
|
+
var DEFAULT_TIMEOUT2 = 5e3;
|
|
447
|
+
var TOKEN_TTL_MS = 3e4;
|
|
448
|
+
var TOKEN_REFRESH_BUFFER_MS = 5e3;
|
|
449
|
+
var CLEANUP_INTERVAL_MS = 6e4;
|
|
450
|
+
var challengeCache = /* @__PURE__ */ new Map();
|
|
451
|
+
var cleanupScheduled = false;
|
|
452
|
+
function scheduleCleanup() {
|
|
453
|
+
if (cleanupScheduled) return;
|
|
454
|
+
cleanupScheduled = true;
|
|
455
|
+
setTimeout(() => {
|
|
456
|
+
const now = Date.now();
|
|
457
|
+
for (const [key, entry] of challengeCache) {
|
|
458
|
+
if (now >= entry.expiresAt) {
|
|
459
|
+
challengeCache.delete(key);
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
cleanupScheduled = false;
|
|
463
|
+
if (challengeCache.size > 0) {
|
|
464
|
+
scheduleCleanup();
|
|
465
|
+
}
|
|
466
|
+
}, CLEANUP_INTERVAL_MS).unref();
|
|
467
|
+
}
|
|
468
|
+
function clearChallengeCache() {
|
|
469
|
+
challengeCache.clear();
|
|
470
|
+
}
|
|
471
|
+
function sendUDP(host, port, data, timeout) {
|
|
472
|
+
return new Promise((resolve, reject) => {
|
|
473
|
+
const socket = import_node_dgram2.default.createSocket("udp4");
|
|
474
|
+
let timeoutHandle;
|
|
475
|
+
let closed = false;
|
|
476
|
+
const cleanup = () => {
|
|
477
|
+
if (closed) return;
|
|
478
|
+
closed = true;
|
|
479
|
+
clearTimeout(timeoutHandle);
|
|
480
|
+
try {
|
|
481
|
+
socket.close();
|
|
482
|
+
} catch {
|
|
483
|
+
}
|
|
484
|
+
};
|
|
485
|
+
socket.on("message", (msg) => {
|
|
486
|
+
cleanup();
|
|
487
|
+
resolve(msg);
|
|
488
|
+
});
|
|
489
|
+
socket.on("error", (err) => {
|
|
490
|
+
cleanup();
|
|
491
|
+
reject(err);
|
|
492
|
+
});
|
|
493
|
+
timeoutHandle = setTimeout(() => {
|
|
494
|
+
cleanup();
|
|
495
|
+
reject(new Error(`Query timeout after ${timeout}ms`));
|
|
496
|
+
}, timeout);
|
|
497
|
+
socket.send(data, port, host, (err) => {
|
|
498
|
+
if (err) {
|
|
499
|
+
cleanup();
|
|
500
|
+
reject(err);
|
|
501
|
+
}
|
|
502
|
+
});
|
|
503
|
+
});
|
|
504
|
+
}
|
|
505
|
+
async function fetchChallenge(host, port, timeout) {
|
|
506
|
+
const request = buildChallengeRequest();
|
|
507
|
+
const response = await sendUDP(host, port, request, timeout);
|
|
508
|
+
return parseChallengeResponse(response);
|
|
509
|
+
}
|
|
510
|
+
async function ensureChallenge(host, port, timeout) {
|
|
511
|
+
const key = `${host}:${port}`;
|
|
512
|
+
const cached = challengeCache.get(key);
|
|
513
|
+
const now = Date.now();
|
|
514
|
+
if (cached && now < cached.expiresAt - TOKEN_REFRESH_BUFFER_MS) {
|
|
515
|
+
cached.requestId++;
|
|
516
|
+
return cached;
|
|
517
|
+
}
|
|
518
|
+
const token = await fetchChallenge(host, port, timeout);
|
|
519
|
+
const entry = {
|
|
520
|
+
token,
|
|
521
|
+
expiresAt: now + TOKEN_TTL_MS,
|
|
522
|
+
requestId: 1
|
|
523
|
+
};
|
|
524
|
+
challengeCache.set(key, entry);
|
|
525
|
+
scheduleCleanup();
|
|
526
|
+
return entry;
|
|
527
|
+
}
|
|
528
|
+
async function performV2Query(host, port, type, offset, timeout, authToken) {
|
|
529
|
+
const challenge = await ensureChallenge(host, port, timeout);
|
|
530
|
+
const request = buildV2Request(type, challenge.token, challenge.requestId, 0, offset, authToken);
|
|
531
|
+
const response = await sendUDP(host, port, request, timeout);
|
|
532
|
+
return parseV2Response(response);
|
|
533
|
+
}
|
|
534
|
+
async function queryV2(host, port = 5520, options = {}) {
|
|
535
|
+
const timeout = options.timeout ?? DEFAULT_TIMEOUT2;
|
|
536
|
+
const wantPlayers = options.players === true || options.players === "all";
|
|
537
|
+
const offset = options.playerOffset ?? 0;
|
|
538
|
+
const { header: basicHeader, entries: basicEntries } = await performV2Query(
|
|
539
|
+
host,
|
|
540
|
+
port,
|
|
541
|
+
V2QueryType.BASIC,
|
|
542
|
+
0,
|
|
543
|
+
timeout,
|
|
544
|
+
options.authToken
|
|
545
|
+
);
|
|
546
|
+
const hasAddress = (basicHeader.flags & V2ResponseFlag.HAS_ADDRESS) !== 0;
|
|
547
|
+
const isNetwork = (basicHeader.flags & V2ResponseFlag.IS_NETWORK) !== 0;
|
|
548
|
+
const serverInfoEntry = findTLVEntry(basicEntries, V2TLVType.SERVER_INFO);
|
|
549
|
+
if (!serverInfoEntry) {
|
|
550
|
+
throw new Error("Response missing SERVER_INFO TLV");
|
|
551
|
+
}
|
|
552
|
+
const serverInfo = parseTLVServerInfo(serverInfoEntry, hasAddress, isNetwork);
|
|
553
|
+
if (!wantPlayers) {
|
|
554
|
+
return serverInfo;
|
|
555
|
+
}
|
|
556
|
+
const { header: playersHeader, entries: playersEntries } = await performV2Query(
|
|
557
|
+
host,
|
|
558
|
+
port,
|
|
559
|
+
V2QueryType.PLAYERS,
|
|
560
|
+
offset,
|
|
561
|
+
timeout,
|
|
562
|
+
options.authToken
|
|
563
|
+
);
|
|
564
|
+
const hasMorePlayers = (playersHeader.flags & V2ResponseFlag.HAS_MORE_PLAYERS) !== 0;
|
|
565
|
+
const playerListEntry = findTLVEntry(playersEntries, V2TLVType.PLAYER_LIST);
|
|
566
|
+
if (!playerListEntry) {
|
|
567
|
+
throw new Error("Response missing PLAYER_LIST TLV");
|
|
568
|
+
}
|
|
569
|
+
const playerList = parseTLVPlayerList(playerListEntry);
|
|
570
|
+
let players = playerList.players;
|
|
571
|
+
let currentOffset = playerList.offset + players.length;
|
|
572
|
+
let hasMore = hasMorePlayers;
|
|
573
|
+
if (options.players === "all" && hasMore) {
|
|
574
|
+
const allPlayers = [...players];
|
|
575
|
+
while (hasMore) {
|
|
576
|
+
const nextResult = await performV2Query(
|
|
577
|
+
host,
|
|
578
|
+
port,
|
|
579
|
+
V2QueryType.PLAYERS,
|
|
580
|
+
currentOffset,
|
|
581
|
+
timeout,
|
|
582
|
+
options.authToken
|
|
583
|
+
);
|
|
584
|
+
const nextHasMore = (nextResult.header.flags & V2ResponseFlag.HAS_MORE_PLAYERS) !== 0;
|
|
585
|
+
const nextPlayerEntry = findTLVEntry(nextResult.entries, V2TLVType.PLAYER_LIST);
|
|
586
|
+
if (!nextPlayerEntry) {
|
|
587
|
+
break;
|
|
588
|
+
}
|
|
589
|
+
const nextPlayerList = parseTLVPlayerList(nextPlayerEntry);
|
|
590
|
+
allPlayers.push(...nextPlayerList.players);
|
|
591
|
+
currentOffset = nextPlayerList.offset + nextPlayerList.players.length;
|
|
592
|
+
hasMore = nextHasMore;
|
|
593
|
+
}
|
|
594
|
+
players = allPlayers;
|
|
595
|
+
hasMore = false;
|
|
596
|
+
}
|
|
597
|
+
return {
|
|
598
|
+
...serverInfo,
|
|
599
|
+
players,
|
|
600
|
+
totalPlayers: playerList.totalPlayers,
|
|
601
|
+
offset: playerList.offset,
|
|
602
|
+
hasMore
|
|
603
|
+
};
|
|
604
|
+
}
|
|
219
605
|
// Annotate the CommonJS export names for ESM import in node:
|
|
220
606
|
0 && (module.exports = {
|
|
221
|
-
|
|
607
|
+
V2QueryType,
|
|
608
|
+
V2ResponseFlag,
|
|
609
|
+
V2TLVType,
|
|
610
|
+
clearChallengeCache,
|
|
611
|
+
query,
|
|
612
|
+
queryV2
|
|
222
613
|
});
|
|
223
614
|
//# sourceMappingURL=index.cjs.map
|