298 lines
8.0 KiB
JavaScript
298 lines
8.0 KiB
JavaScript
const crypto = require("crypto");
|
|
|
|
const DNSResolver = require("./dns");
|
|
const Server = require("./server");
|
|
|
|
const { GRAPH_UPDATE_TIME_GAP, TimeTracker } = require("./time");
|
|
const { getPlayerCountOrNull } = require("./util");
|
|
|
|
const config = require("../config");
|
|
const minecraftVersions = require("../minecraft_versions");
|
|
|
|
class ServerRegistration {
|
|
serverId;
|
|
lastFavicon;
|
|
versions = [];
|
|
recordData;
|
|
graphData = [];
|
|
|
|
constructor(app, serverId, data) {
|
|
this._app = app;
|
|
this.serverId = serverId;
|
|
this.data = data;
|
|
this._pingHistory = [];
|
|
this.dnsResolver = new DNSResolver(this.data.ip, this.data.port);
|
|
}
|
|
|
|
handlePing(timestamp, resp, err, version, updateHistoryGraph) {
|
|
// Use null to represent a failed ping
|
|
const unsafePlayerCount = getPlayerCountOrNull(resp);
|
|
|
|
// Store into in-memory ping data
|
|
TimeTracker.pushAndShift(
|
|
this._pingHistory,
|
|
unsafePlayerCount,
|
|
TimeTracker.getMaxServerGraphDataLength()
|
|
);
|
|
|
|
// Only notify the frontend to append to the historical graph
|
|
// if both the graphing behavior is enabled and the backend agrees
|
|
// that the ping is eligible for addition
|
|
if (updateHistoryGraph) {
|
|
TimeTracker.pushAndShift(
|
|
this.graphData,
|
|
unsafePlayerCount,
|
|
TimeTracker.getMaxGraphDataLength()
|
|
);
|
|
}
|
|
|
|
// Delegate out update payload generation
|
|
return this.getUpdate(timestamp, resp, err, version);
|
|
}
|
|
|
|
getUpdate(timestamp, resp, err, version) {
|
|
const update = {};
|
|
|
|
// Always append a playerCount value
|
|
// When resp is undefined (due to an error), playerCount will be null
|
|
update.playerCount = getPlayerCountOrNull(resp);
|
|
|
|
if (resp) {
|
|
if (
|
|
resp.version &&
|
|
this.updateProtocolVersionCompat(
|
|
resp.version,
|
|
version.protocolId,
|
|
version.protocolIndex
|
|
)
|
|
) {
|
|
// Append an updated version listing
|
|
update.versions = this.versions;
|
|
}
|
|
|
|
if (
|
|
config.logToDatabase &&
|
|
(!this.recordData || resp.players.online > this.recordData.playerCount)
|
|
) {
|
|
this.recordData = {
|
|
playerCount: resp.players.online,
|
|
timestamp: TimeTracker.toSeconds(timestamp),
|
|
};
|
|
|
|
// Append an updated recordData
|
|
update.recordData = this.recordData;
|
|
|
|
// Update record in database
|
|
this._app.database.updatePlayerCountRecord(
|
|
this.data.ip,
|
|
resp.players.online,
|
|
timestamp
|
|
);
|
|
}
|
|
|
|
if (this.updateFavicon(resp.favicon)) {
|
|
update.favicon = this.getFaviconUrl();
|
|
}
|
|
|
|
if (config.logToDatabase) {
|
|
// Update calculated graph peak regardless if the graph is being updated
|
|
// This can cause a (harmless) desync between live and stored data, but it allows it to be more accurate for long surviving processes
|
|
if (this.findNewGraphPeak()) {
|
|
update.graphPeakData = this.getGraphPeak();
|
|
}
|
|
}
|
|
} else if (err) {
|
|
// Append a filtered copy of err
|
|
// This ensures any unintended data is not leaked
|
|
update.error = this.filterError(err);
|
|
}
|
|
|
|
return update;
|
|
}
|
|
|
|
getPingHistory() {
|
|
if (this._pingHistory.length > 0) {
|
|
const payload = {
|
|
versions: this.versions,
|
|
recordData: this.recordData,
|
|
favicon: this.getFaviconUrl(),
|
|
};
|
|
|
|
// Only append graphPeakData if defined
|
|
// The value is lazy computed and conditional that config->logToDatabase == true
|
|
const graphPeakData = this.getGraphPeak();
|
|
|
|
if (graphPeakData) {
|
|
payload.graphPeakData = graphPeakData;
|
|
}
|
|
|
|
// Assume the ping was a success and define result
|
|
// pingHistory does not keep error references, so its impossible to detect if this is an error
|
|
// It is also pointless to store that data since it will be short lived
|
|
payload.playerCount = this._pingHistory[this._pingHistory.length - 1];
|
|
|
|
// Send a copy of pingHistory
|
|
// Include the last value even though it is contained within payload
|
|
// The frontend will only push to its graphData from playerCountHistory
|
|
payload.playerCountHistory = this._pingHistory;
|
|
|
|
return payload;
|
|
}
|
|
|
|
return {
|
|
error: {
|
|
message: "Pinging...",
|
|
},
|
|
recordData: this.recordData,
|
|
graphPeakData: this.getGraphPeak(),
|
|
favicon: this.data.favicon,
|
|
};
|
|
}
|
|
|
|
loadGraphPoints(startTime, timestamps, points) {
|
|
this.graphData = TimeTracker.everyN(
|
|
timestamps,
|
|
startTime,
|
|
GRAPH_UPDATE_TIME_GAP,
|
|
(i) => points[i]
|
|
);
|
|
}
|
|
|
|
findNewGraphPeak() {
|
|
let index = -1;
|
|
for (let i = 0; i < this.graphData.length; i++) {
|
|
const point = this.graphData[i];
|
|
if (point !== null && (index === -1 || point > this.graphData[index])) {
|
|
index = i;
|
|
}
|
|
}
|
|
if (index >= 0) {
|
|
const lastGraphPeakIndex = this._graphPeakIndex;
|
|
this._graphPeakIndex = index;
|
|
return index !== lastGraphPeakIndex;
|
|
} else {
|
|
this._graphPeakIndex = undefined;
|
|
return false;
|
|
}
|
|
}
|
|
|
|
getGraphPeak() {
|
|
if (this._graphPeakIndex === undefined) {
|
|
return;
|
|
}
|
|
return {
|
|
playerCount: this.graphData[this._graphPeakIndex],
|
|
timestamp: this._app.timeTracker.getGraphPointAt(this._graphPeakIndex),
|
|
};
|
|
}
|
|
|
|
updateFavicon(favicon) {
|
|
// If data.favicon is defined, then a favicon override is present
|
|
// Disregard the incoming favicon, regardless if it is different
|
|
if (this.data.favicon) {
|
|
return false;
|
|
}
|
|
|
|
if (favicon && favicon !== this.lastFavicon) {
|
|
this.lastFavicon = favicon;
|
|
|
|
// Generate an updated hash
|
|
// This is used by #getFaviconUrl
|
|
this.faviconHash = crypto
|
|
.createHash("md5")
|
|
.update(favicon)
|
|
.digest("hex")
|
|
.toString();
|
|
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
getFaviconUrl() {
|
|
if (this.faviconHash) {
|
|
return Server.getHashedFaviconUrl(this.faviconHash);
|
|
} else if (this.data.favicon) {
|
|
return this.data.favicon;
|
|
}
|
|
}
|
|
|
|
updateProtocolVersionCompat(incomingId, outgoingId, protocolIndex) {
|
|
// If the result version matches the attempted version, the version is supported
|
|
const isSuccess = incomingId === outgoingId;
|
|
const indexOf = this.versions.indexOf(protocolIndex);
|
|
|
|
// Test indexOf to avoid inserting previously recorded protocolIndex values
|
|
if (isSuccess && indexOf < 0) {
|
|
this.versions.push(protocolIndex);
|
|
|
|
// Sort versions in ascending order
|
|
// This matches protocol ids to Minecraft versions release order
|
|
this.versions.sort((a, b) => a - b);
|
|
|
|
return true;
|
|
} else if (!isSuccess && indexOf >= 0) {
|
|
this.versions.splice(indexOf, 1);
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
getNextProtocolVersion() {
|
|
// Minecraft Bedrock Edition does not have protocol versions
|
|
if (this.data.type === "PE") {
|
|
return {
|
|
protocolId: 0,
|
|
protocolIndex: 0,
|
|
};
|
|
}
|
|
const protocolVersions = minecraftVersions[this.data.type];
|
|
if (
|
|
typeof this._nextProtocolIndex === "undefined" ||
|
|
this._nextProtocolIndex + 1 >= protocolVersions.length
|
|
) {
|
|
this._nextProtocolIndex = 0;
|
|
} else {
|
|
this._nextProtocolIndex++;
|
|
}
|
|
return {
|
|
protocolId: protocolVersions[this._nextProtocolIndex].protocolId,
|
|
protocolIndex: this._nextProtocolIndex,
|
|
};
|
|
}
|
|
|
|
filterError(err) {
|
|
let message = "Unknown error";
|
|
|
|
// Attempt to match to the first possible value
|
|
for (const key of ["message", "description", "errno"]) {
|
|
if (err[key]) {
|
|
message = err[key];
|
|
break;
|
|
}
|
|
}
|
|
|
|
// Trim the message if too long
|
|
if (message.length > 28) {
|
|
message = message.substring(0, 28) + "...";
|
|
}
|
|
|
|
return {
|
|
message: message,
|
|
};
|
|
}
|
|
|
|
getPublicData() {
|
|
// Return a custom object instead of data directly to avoid data leakage
|
|
return {
|
|
name: this.data.name,
|
|
ip: this.data.ip,
|
|
type: this.data.type,
|
|
color: this.data.color,
|
|
};
|
|
}
|
|
}
|
|
|
|
module.exports = ServerRegistration;
|