Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions docker-compose-dev.yml
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ services:
context: .
ports:
- 48000:48000
network_mode: bridge
volumes:
- /var/run/docker.sock:/var/run/docker.sock
- .data:/var/task/server/data
Expand Down
1 change: 1 addition & 0 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ services:
image: ghcr.io/crafty-monster/rocky
ports:
- 48000:48000
network_mode: bridge
volumes:
- /var/run/docker.sock:/var/run/docker.sock
- .data:/var/task/server/data
Expand Down
16 changes: 14 additions & 2 deletions package-lock.json

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

98 changes: 98 additions & 0 deletions server/lib/status.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
import udp from 'dgram';
import Cache from 'ttl';

const MAX_WAIT_MS = 1 * 1000;
const OFFLINE_MESSAGE_DATA_ID = '00ffff00fefefefefdfdfdfd12345678';
const UNCONNECTED_PING = // see https://wiki.vg/Raknet_Protocol#Unconnected_Ping
'01' + '0000000000000000' + OFFLINE_MESSAGE_DATA_ID + '0000000000000000'; // fmt: skip;

const cache = new Cache({ttl: 2 * 60 * 1000}); // 2 minute cache

/**
* Minecraft Server status abstraction
*/
export default class Status {
/**
* Checks minecraft status from remote UDP host
* @param {string} host the minecraft server address
* @param {string|Number} port the minecraft server port
*/
static async check(host = 'localhost', port) {
console.log('Status.check(%s, %s)', host, port);
const key = `${host}:${port}`;
const cached = cache.get(key);
if (cached) return cached;
try {
// Not in cache?
// Go check instead
const buffer = await UDP.send(host, port, Buffer.from(UNCONNECTED_PING, 'hex'));
// console.log('---------------------------');
// console.log('Parsing...');
// https://wiki.vg/Raknet_Protocol#Unconnected_Pong
const packet = {
type: buffer.slice(0, 1), // byte (1)
time: buffer.slice(1, 9), // long (8)
guid: buffer.slice(9, 17), // long (8)
magic: buffer.slice(17, 33), // magic (16)
data: buffer.slice(35), // string (*)
};
// console.log({packet});
// console.log(packet.data.toString('utf8'));
if (!Buffer.from(OFFLINE_MESSAGE_DATA_ID, 'hex').equals(packet.magic)) {
throw new Error('Invalid server reponse');
}
const data = packet.data?.toString('utf8')?.split(';') || '';
const [edition, motd1, protocol, version, count, max, uid, motd2, mode, no, portipv4, portipv6] = data;
const status = {edition, motd1, protocol, version, count, max, uid, motd2, mode, no, portipv4, portipv6};
// console.log({status});
cache.put(key, status);
return status;
} catch (err) {
console.error(err);
}
return undefined;
}
}

/**
* UDP abstraction
*/
class UDP {
/**
* Send a datagram packet and wait for respose.
* @param {string} host The remote host address
* @param {string|Number} port The port to connect to
* @param {string|Buffer} message The message to send
* @return {Buffer} The returned udp message
*/
static async send(host, port, message) {
console.log('UDP.send(%s, %s, message)', host, port);
const buffer = Buffer.from(message);
// console.log(`Sending '${buffer.toString('hex')}' to ${host}:${port}`);
return new Promise((resolve, reject) => {
const client = udp.createSocket('udp4');
const timeout = setTimeout(() => onError(new Error('UDP: Timeout. No response.')), MAX_WAIT_MS);
client.on('error', onError);
client.send(buffer, port, host, onError);
client.on('message', (msg, info) => {
// console.log('---------------------------');
// console.log('Received %d bytes from %s:%d', msg.length, info.address, info.port);
// console.log(`-->${msg.toString('hex')}`);
// console.log(`-->${msg.toString('utf8')}`);
client.close();
clearTimeout(timeout);
resolve(msg);
});
// eslint-disable-next-line require-jsdoc
function onError(e) {
if (e) {
console.error('UPD Error: ' + e);
client.close();
clearTimeout(timeout);
reject(e);
}
}
});
}
}

8 changes: 5 additions & 3 deletions server/lib/udp.cjs → server/lib/udp.server.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ const udp = require('dgram');

// creating a udp server
const server = udp.createSocket('udp4');
const PORT = 19132;
const PORT = 19000;

// emits when any error occurs
server.on('error', function(error) {
Expand All @@ -24,10 +24,12 @@ server.on('message', function(msg, info) {
console.log('Received %d bytes from %s:%d', msg.length, info.address, info.port);
console.log(msg.toString());

// sending msg
server.send(msg, info.port, 'localhost', function(error) {
// echo msg back to client
server.send(msg, info.port, info.address, function(error, bytes) {
if (error) {
console.error('Error sending response');
} else {
console.log(`${bytes} bytes sent back to client!`);
}
console.log('---------------------------');
});
Expand Down
88 changes: 88 additions & 0 deletions server/lib/udp.status.cjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
/* eslint-disable require-jsdoc */
// @see https://github.com/py-mine/mcstatus/blob/master/mcstatus/bedrock_status.py
const udp = require('dgram');
const port = 48548; // 19000;
const host = 'localhost';

const MAX_WAIT_MS = 2000;
const OFFLINE_MESSAGE_DATA_ID = '00ffff00fefefefefdfdfdfd12345678';
const UNCONNECTED_PING = // see https://wiki.vg/Raknet_Protocol#Unconnected_Ping
'01' + '0000000000000000' + OFFLINE_MESSAGE_DATA_ID + '0000000000000000'; // fmt: skip;

/**
* Minecraft Server status abstraction
*/
class Status {
/**
* Fetches status from remote UDP host
* @param {string} host the minecraft server address
* @param {string|Number} port the minecraft server port
*/
static async fetch(host, port) {
console.log('Status.fetch(%s, %s)', host, port);
const buffer = await UDP.send(host, port, Buffer.from(UNCONNECTED_PING, 'hex'));
console.log('---------------------------');
console.log('Parsing...');
// https://wiki.vg/Raknet_Protocol#Unconnected_Pong
const packet = {
type: buffer.slice(0, 1), // byte
time: buffer.slice(1, 9), // long
guid: buffer.slice(9, 17), // long
magic: buffer.slice(17, 33), // magic
data: buffer.slice(35), // string
};
console.log({packet});
console.log(packet.data.toString('utf8'));
const data = packet.data?.toString('utf8')?.split(';') || '';
const [edition, motd1, protocol, version, count, max, uid, motd2, mode, no, portipv4, portipv6] = data;
const status = {
edition, motd1, protocol, version, count, max, uid, motd2, mode, no, portipv4, portipv6,
};
console.log({status});
}
}

/**
* UDP abstraction
*/
class UDP {
/**
* Send a datagram packet and wait for respose.
* @param {string} host The remote host address
* @param {string|Number} port The port to connect to
* @param {string|Buffer} message The message to send
* @return {Buffer} The returned udp message
*/
static async send(host, port, message) {
const buffer = Buffer.from(message);
console.log(`Sending '${buffer.toString('hex')}' to ${host}:${port}`);
return new Promise((resolve, reject) => {
const client = udp.createSocket('udp4');
const timeout = setTimeout(() => onError(new Error('UDP: Timeout. No response.')), MAX_WAIT_MS);
client.on('error', onError);
client.send(buffer, port, host, onError);
client.on('message', (msg, info) => {
console.log('---------------------------');
console.log('Received %d bytes from %s:%d', msg.length, info.address, info.port);
console.log(`-->${msg.toString('hex')}`);
console.log(`-->${msg.toString('utf8')}`);
client.close();
clearTimeout(timeout);
resolve(msg);
});
// eslint-disable-next-line require-jsdoc
function onError(e) {
if (e) {
console.error('UPD Error: ' + e);
client.close();
clearTimeout(timeout);
reject(e);
}
}
});
}
}

// Go
Status.fetch(host, port);

35 changes: 31 additions & 4 deletions server/lib/world.js
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
/* eslint-disable require-jsdoc */
import randomQuotes from 'random-quotes';
import {statusBedrock} from 'minecraft-server-util';
import server from './server.js';
import Server from './server.js';
import Status from './status.js';
import config from './config.js';
import utils from '../../utils/index.js';

const {DOCKER_HOST, ROCKY_SERVER_IMAGE, ROCKY_MAX_WORLDS, ROCKY_MAX_WORLDS_PER_USER} = config;
const docker = server.docker;
const docker = Server.docker;

export default class World {
/**
Expand Down Expand Up @@ -89,8 +90,11 @@ export default class World {
const folder = c.Mounts?.find(m => m.Type === 'bind')?.Source;
const created = new Date(c.Created * 1000).getTime();
const by = c.Labels['monster.crafty.rocky.by'];
const players = c.status?.count;
const max = c.status?.max;
const version = c.status?.version;
const meta = c;
return {id, name, description, port, state, folder, created, by, meta};
return {id, name, description, port, state, folder, created, by, players, max, version, meta};
}

/**
Expand All @@ -106,7 +110,24 @@ export default class World {
const port = c.Ports?.[0]?.PublicPort;
const created = new Date(c.Created * 1000).toISOString();
const by = c.Labels['monster.crafty.rocky.by'];
return {id, name, description, created, port, by};
const players = c.status?.count;
const max = c.status?.max;
const version = c.status?.version;
return {id, name, description, created, port, by, players, max, version};
}

/**
* Adds the status for each container
* @param {Object} c the container
* @return {Object} the container, with status information
*/
static async addStatus(c) {
const ip = c.NetworkSettings?.Networks?.bridge?.IPAddress;
const port = c.Ports?.[0]?.PrivatePort;
if (ip && port) {
c.status = await Status.check(ip, port);
}
return c;
}

/**
Expand All @@ -116,6 +137,9 @@ export default class World {
static async list() {
console.log('World.list()');
const containers = await docker.listContainers({all: true, filters: {name: ['/rocky_world__']}});
for (const c of containers) {
await World.addStatus(c);
}
return containers
.sort((c1, c2) => c1.Created - c2.Created)
.map(World.map);
Expand All @@ -132,6 +156,9 @@ export default class World {
try {
containers = await docker.listContainers({filters: {name: ['/rocky_world__']}});
} catch (err) {/* do nothing */}
for (const c of containers) {
await World.addStatus(c);
}
docker.modem.timeout = null;
return containers
.sort((c1, c2) => c1.Created - c2.Created)
Expand Down
3 changes: 2 additions & 1 deletion server/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,8 @@
"express-rate-limit": "^6.7.0",
"lowdb": "^7.0.1",
"minecraft-server-util": "5.4.2",
"random-quotes": "^1.3.0"
"random-quotes": "^1.3.0",
"ttl": "^1.3.1"
},
"devDependencies": {
"eslint": "^8.37.0",
Expand Down