Skip to content

Commit

Permalink
no longer reuse UDP sockets, since that seems to break randomly in so…
Browse files Browse the repository at this point in the history
…me setups
  • Loading branch information
KurtThiemann committed Nov 28, 2024
1 parent 4b6effc commit 2bdf979
Show file tree
Hide file tree
Showing 9 changed files with 101 additions and 222 deletions.
8 changes: 0 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,14 +26,10 @@ let client = new QueryClient();

let basic = await client.queryBasic('localhost', 25565, AbortSignal.timeout(5000));
let full = await client.queryFull('localhost', 25565, AbortSignal.timeout(5000));

await client.close();
```
Basic and full query requests will return a [`BasicStatResponse`](src/Packet/Query/BasicStatResponse.js)
and [`FullStatResponse`](src/Packet/Query/FullStatResponse.js) object respectively.

Note that the query client needs to be closed manually, since it keeps its UDP socket open to reuse it for future queries.

### Java Edition Ping
The [Server List Ping protocol](https://wiki.vg/Server_List_Ping) is what the Minecraft client uses to show the server status in the in-game server list.
This protocol changed multiple times over the years, so you'd ideally want to know the version of the server you are pinging to use the correct protocol version.
Expand Down Expand Up @@ -98,9 +94,5 @@ import {BedrockPingClient} from 'craftping';
let client = new BedrockPingClient();

let status = await client.ping('localhost', 19132, AbortSignal.timeout(5000));

await client.close();
```
Pinging a Bedrock server will return an [`UnconnectedPong`](src/Packet/BedrockPing/UnconnectedPong.js) object.

Note that the Bedrock ping client also needs to be closed manually, since it keeps its UDP socket open to reuse it for future requests.
10 changes: 5 additions & 5 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "craftping",
"type": "module",
"version": "1.1.3",
"version": "2.0.0",
"main": "index.js",
"repository": "github:aternosorg/craftping",
"scripts": {
Expand Down
25 changes: 2 additions & 23 deletions src/BedrockPing/BedrockPing.js
Original file line number Diff line number Diff line change
@@ -1,37 +1,16 @@
import UDPClient from "../UDPSocket/UDPClient.js";
import UnconnectedPing from "../Packet/BedrockPing/UnconnectedPing.js";
import UnconnectedPong from "../Packet/BedrockPing/UnconnectedPong.js";
import * as crypto from "node:crypto";

export default class BedrockPing extends UDPClient {
/** @type {BigInt} */ sessionId;

/**
* @inheritDoc
*/
appliesTo(message) {
let data = message.getData();
if (data.byteLength < 9) {
return false;
}

let timestamp = data.readBigInt64BE(1);
return timestamp === this.sessionId;
}

/**
* @return {Promise<UnconnectedPong>}
*/
async ping() {
// Normally, the time field is used for the current ms timestamp, but we're using it as a session ID
// to identify which reply belongs to which request since we're using the same socket for multiple requests.
this.sessionId = crypto.randomBytes(8).readBigInt64BE();
let startTime = BigInt(Date.now());
await this.send(new UnconnectedPing().setTime(this.sessionId).generateClientGUID().write());
await this.sendPacket(new UnconnectedPing().setTime(startTime).generateClientGUID());
this.signal?.throwIfAborted();

// The time field in the response contains the session ID, but we replace it with the start time
// in case anyone relies on the time field containing an actual timestamp.
return new UnconnectedPong().read(await this.readData()).setTime(startTime);
return new UnconnectedPong().read(await this.readData());
}
}
22 changes: 17 additions & 5 deletions src/BedrockPing/BedrockPingClient.js
Original file line number Diff line number Diff line change
@@ -1,24 +1,36 @@
import UDPSocket from "../UDPSocket/UDPSocket.js";
import BedrockPing from "./BedrockPing.js";

export default class BedrockPingClient extends UDPSocket {
export default class BedrockPingClient {
/** @type {import("node:dgram").SocketOptions}} */ socketOptions;
/** @type {import("node:dgram").BindOptions}} */ bindOptions;

/**
* @param {import("node:dgram").SocketOptions} socketOptions
* @param {import("node:dgram").BindOptions} bindOptions
*/
constructor(socketOptions = {type: "udp4"}, bindOptions = {}) {
this.socketOptions = socketOptions;
this.bindOptions = bindOptions;
}

/**
* @param {string} address
* @param {number} port
* @param {?AbortSignal} signal
* @return {Promise<UnconnectedPong>}
*/
async ping(address, port, signal = null) {
let ping = new BedrockPing(address, port, this, signal);
await ping.connect();
let ping = new BedrockPing(address, port, signal, this.socketOptions, this.bindOptions);
await ping.bind(signal);
let result;
try {
result = await ping.ping();
} catch (e) {
ping.close();
await ping.close();
throw e;
}
ping.close();
await ping.close();
return result;
}
}
27 changes: 6 additions & 21 deletions src/Query/Query.js
Original file line number Diff line number Diff line change
@@ -1,36 +1,23 @@
import UDPClient from "../UDPSocket/UDPClient.js";
import HandshakeRequest from "../Packet/Query/HandshakeRequest.js";
import HandshakeResponse from "../Packet/Query/HandshakeResponse.js";
import FullStatRequest from "../Packet/Query/FullStatRequest.js";
import FullStatResponse from "../Packet/Query/FullStatResponse.js";
import BasicStatRequest from "../Packet/Query/BasicStatRequest.js";
import BasicStatResponse from "../Packet/Query/BasicStatResponse.js";
import UDPClient from "../UDPSocket/UDPClient.js";

export default class Query extends UDPClient {
/** @type {number} */ sessionId;
/** @type {number} */ challengeToken;

/**
* @inheritDoc
*/
appliesTo(message) {
let data = message.getData();
if (data.byteLength < 5) {
return false;
}

let session = data.readUInt32BE(1);
return session === this.sessionId;
}

/**
* @return {Promise<this>}
*/
async handshake() {
let handshakeRequest = new HandshakeRequest().generateSessionId();
this.sessionId = handshakeRequest.getSessionId();

await this.send(handshakeRequest.write());
await this.sendPacket(handshakeRequest);
this.signal?.throwIfAborted();

let handshakeResponse = new HandshakeResponse().read(await this.readData());
Expand All @@ -44,10 +31,9 @@ export default class Query extends UDPClient {
async queryBasic() {
await this.handshake();

await this.send(new BasicStatRequest()
await this.sendPacket(new BasicStatRequest()
.setSessionId(this.sessionId)
.setChallengeToken(this.challengeToken)
.write());
.setChallengeToken(this.challengeToken));

this.signal?.throwIfAborted();
return new BasicStatResponse().read(await this.readData());
Expand All @@ -59,10 +45,9 @@ export default class Query extends UDPClient {
async queryFull() {
await this.handshake();

await this.send(new FullStatRequest()
await this.sendPacket(new FullStatRequest()
.setSessionId(this.sessionId)
.setChallengeToken(this.challengeToken)
.write());
.setChallengeToken(this.challengeToken));

this.signal?.throwIfAborted();
return new FullStatResponse().read(await this.readData());
Expand Down
31 changes: 21 additions & 10 deletions src/Query/QueryClient.js
Original file line number Diff line number Diff line change
@@ -1,24 +1,35 @@
import UDPSocket from "../UDPSocket/UDPSocket.js";
import Query from "./Query.js";

export default class QueryClient extends UDPSocket {
export default class QueryClient {
/** @type {import("node:dgram").SocketOptions}} */ socketOptions;
/** @type {import("node:dgram").BindOptions}} */ bindOptions;

/**
* @param {import("node:dgram").SocketOptions} socketOptions
* @param {import("node:dgram").BindOptions} bindOptions
*/
constructor(socketOptions = {type: "udp4"}, bindOptions = {}) {
this.socketOptions = socketOptions;
this.bindOptions = bindOptions;
}

/**
* @param {string} address
* @param {number} port
* @param {?AbortSignal} signal
* @return {Promise<BasicStatResponse>}
*/
async queryBasic(address, port, signal = null) {
let query = new Query(address, port, this, signal);
await query.connect();
let query = new Query(address, port, signal, this.socketOptions, this.bindOptions);
await query.bind(signal);
let result;
try {
result = await query.queryBasic();
} catch (e) {
query.close();
await query.close();
throw e;
}
query.close();
await query.close();
return result;
}

Expand All @@ -29,16 +40,16 @@ export default class QueryClient extends UDPSocket {
* @return {Promise<FullStatResponse>}
*/
async queryFull(address, port, signal = null) {
let query = new Query(address, port, this, signal);
await query.connect();
let query = new Query(address, port, signal, this.socketOptions, this.bindOptions);
await query.bind(signal);
let result;
try {
result = await query.queryFull();
} catch (e) {
query.close();
await query.close();
throw e;
}
query.close();
await query.close();
return result;
}
}
Loading

0 comments on commit 2bdf979

Please sign in to comment.