diff --git a/.github/workflows/pr-trusted.yml b/.github/workflows/pr-trusted.yml index 857b021a..afc4f319 100644 --- a/.github/workflows/pr-trusted.yml +++ b/.github/workflows/pr-trusted.yml @@ -25,6 +25,9 @@ jobs: with: app-id: ${{ secrets.FISH_BOT_ID }} private-key: ${{ secrets.FISH_BOT_PRIVATE_KEY }} + - uses: actions/setup-node@v4 + with: + node-version: '22.x' # Checkout the trusted code - uses: actions/checkout@v1 with: diff --git a/build/scripts/config.js b/build/scripts/config.js index fecc34a2..b23c05ae 100644 --- a/build/scripts/config.js +++ b/build/scripts/config.js @@ -33,7 +33,7 @@ var __spreadArray = (this && this.__spreadArray) || function (to, from, pack) { return to.concat(ar || Array.prototype.slice.call(from)); }; Object.defineProperty(exports, "__esModule", { value: true }); -exports.rules = exports.tips = exports.FColor = exports.text = exports.prefixes = exports.Gamemode = exports.FishServer = exports.mapRepoURLs = exports.backendIP = exports.Mode = exports.stopAntiEvadeTime = exports.heuristics = exports.adminNames = exports.multiCharSubstitutions = exports.substitutions = exports.bannedWords = void 0; +exports.localIPAddress = exports.rules = exports.tips = exports.FColor = exports.text = exports.prefixes = exports.Gamemode = exports.FishServer = exports.mapRepoURLs = exports.backendIP = exports.Mode = exports.stopAntiEvadeTime = exports.heuristics = exports.adminNames = exports.multiCharSubstitutions = exports.substitutions = exports.bannedWords = void 0; var globals_1 = require("./globals"); var ranks_1 = require("./ranks"); var funcs_1 = require("./funcs"); @@ -335,5 +335,6 @@ exports.rules = [ "# 8: [pink]No trolling or intentionally causing chaos. This includes any actions or messages that disrupt the community or create an unpleasant atmosphere.", "Failure to follow these rules will result in consequences: likely a Marked Griefer tag for any game disruption, mute for broken chat rules, and bans for repeated offenses or bypasses." ].map(function (r) { return "[white]".concat(r); }); -var templateObject_1, templateObject_2, templateObject_3, templateObject_4, templateObject_5, templateObject_6, templateObject_7, templateObject_8, templateObject_9; //#endregion +exports.localIPAddress = (0, funcs_1.getIPAddress)(); +var templateObject_1, templateObject_2, templateObject_3, templateObject_4, templateObject_5, templateObject_6, templateObject_7, templateObject_8, templateObject_9; diff --git a/build/scripts/consoleCommands.js b/build/scripts/consoleCommands.js index 69fefc44..fb9499ab 100644 --- a/build/scripts/consoleCommands.js +++ b/build/scripts/consoleCommands.js @@ -112,7 +112,7 @@ exports.commands = (0, commands_1.consoleCommandList)({ var _loop_1 = function (playerInfo) { var fishP = players_1.FishPlayer.getById(playerInfo.id); outputString.push("Trace info for player &y".concat(playerInfo.id, "&fr / &c\"").concat(Strings.stripColors(playerInfo.lastName), "\" &lk(").concat(playerInfo.lastName, ")&fr\n\tall names used: ").concat(playerInfo.names.map(function (n) { return "&c\"".concat(n, "\"&fr"); }).items.join(', '), "\n\tall IPs used: ").concat(playerInfo.ips.map(function (n) { return (n == playerInfo.lastIP ? '&c' : '&w') + n + '&fr'; }).items.join(", "), "\n\tjoined &c").concat(playerInfo.timesJoined, "&fr times, kicked &c").concat(playerInfo.timesKicked, "&fr times") - + (fishP ? "\n\tUSID: &c".concat(fishP.usid, "&fr\n\tRank: &c").concat(fishP.rank.name, "&fr\n\tMarked: ").concat(fishP.marked() ? "&runtil ".concat((0, utils_1.formatTimeRelative)(fishP.unmarkTime)) : fishP.autoflagged ? "&rautoflagged" : "&gfalse", "&fr\n\tMuted: &c").concat(f.boolBad(fishP.muted), "&fr") + + (fishP ? "\n\tUSID: &c".concat(fishP.usid(), "&fr\n\tRank: &c").concat(fishP.rank.name, "&fr\n\tMarked: ").concat(fishP.marked() ? "&runtil ".concat((0, utils_1.formatTimeRelative)(fishP.unmarkTime)) : fishP.autoflagged ? "&rautoflagged" : "&gfalse", "&fr\n\tMuted: &c").concat(f.boolBad(fishP.muted), "&fr") : "")); }; try { @@ -143,7 +143,7 @@ exports.commands = (0, commands_1.consoleCommandList)({ var outputString = [""]; var _loop_2 = function (player) { var playerInfo = admins.getInfo(player.uuid); - outputString.push("Info for player &c\"".concat(player.cleanedName, "\" &lk(").concat(player.name, ")&fr\n\tUUID: &c\"").concat(playerInfo.id, "\"&fr\n\tUSID: &c").concat(player.usid ? "\"".concat(player.usid, "\"") : "unknown", "&fr\n\tall names used: ").concat(playerInfo.names.map(function (n) { return "&c\"".concat(n, "\"&fr"); }).items.join(', '), "\n\tall IPs used: ").concat(playerInfo.ips.map(function (n) { return (n == playerInfo.lastIP ? '&c' : '&w') + n + '&fr'; }).items.join(", "), "\n\tjoined &c").concat(playerInfo.timesJoined, "&fr times, kicked &c").concat(playerInfo.timesKicked, "&fr times\n\trank: &c").concat(player.rank.name, "&fr").concat((player.marked() ? ", &lris marked&fr" : "") + (player.muted ? ", &lris muted&fr" : "") + (player.hasFlag("member") ? ", &lmis member&fr" : "") + (player.autoflagged ? ", &lris autoflagged&fr" : ""))); + outputString.push("Info for player &c\"".concat(player.cleanedName, "\" &lk(").concat(player.name, ")&fr\n\tUUID: &c\"").concat(playerInfo.id, "\"&fr\n\tUSID: &c").concat(player.usid() ? "\"".concat(player.usid(), "\"") : "unknown", "&fr\n\tall names used: ").concat(playerInfo.names.map(function (n) { return "&c\"".concat(n, "\"&fr"); }).items.join(', '), "\n\tall IPs used: ").concat(playerInfo.ips.map(function (n) { return (n == playerInfo.lastIP ? '&c' : '&w') + n + '&fr'; }).items.join(", "), "\n\tjoined &c").concat(playerInfo.timesJoined, "&fr times, kicked &c").concat(playerInfo.timesKicked, "&fr times\n\trank: &c").concat(player.rank.name, "&fr").concat((player.marked() ? ", &lris marked&fr" : "") + (player.muted ? ", &lris muted&fr" : "") + (player.hasFlag("member") ? ", &lmis member&fr" : "") + (player.autoflagged ? ", &lris autoflagged&fr" : ""))); }; try { for (var infoList_2 = __values(infoList), infoList_2_1 = infoList_2.next(); !infoList_2_1.done; infoList_2_1 = infoList_2.next()) { @@ -422,7 +422,7 @@ exports.commands = (0, commands_1.consoleCommandList)({ for (var _c = __values(Object.entries(players_1.FishPlayer.cachedPlayers)), _d = _c.next(); !_d.done; _d = _c.next()) { var _e = __read(_d.value, 2), uuid = _e[0], fishP = _e[1]; total++; - fishP.usid = null; + fishP.setUSID(undefined); } } catch (e_3_1) { e_3 = { error: e_3_1 }; } @@ -450,11 +450,24 @@ exports.commands = (0, commands_1.consoleCommandList)({ (_c = players_1.FishPlayer.getById(args.player)) !== null && _c !== void 0 ? _c : (0, commands_1.fail)(admins.getInfoOptional(args.player) ? "Player ".concat(args.player, " has joined the server, but their info was not cached, most likely because they have no rank, so there is no stored USID.") : "Unknown player ".concat(args.player)); - var oldusid = player.usid; - player.usid = null; + if (player.ranksAtLeast("admin")) + (0, commands_1.fail)("Please use the approveauth command instead."); + var oldusid = player.usid(); + player.setUSID(undefined); outputSuccess("Removed the usid of player ".concat(player.name, "/").concat(player.uuid, " (was ").concat(oldusid, ")")); } }, + approveauth: { + args: ["usid:string"], + description: "Sets the USID of a player.", + handler: function (_a) { + var _b; + var args = _a.args, outputSuccess = _a.outputSuccess, f = _a.f; + var player = (_b = players_1.FishPlayer.lastAuthKicked) !== null && _b !== void 0 ? _b : (0, commands_1.fail)("No authorization failures have occurred since the last restart."); + player.setUSID(args.usid); + outputSuccess(f(templateObject_5 || (templateObject_5 = __makeTemplateObject(["Set USID for player ", " to ", "."], ["Set USID for player ", " to ", "."])), player, args.usid)); + } + }, update: { args: ["branch:string?"], description: "Updates the plugin.", @@ -543,7 +556,7 @@ exports.commands = (0, commands_1.consoleCommandList)({ handler: function (_a) { var args = _a.args, f = _a.f, outputSuccess = _a.outputSuccess; if (args.player.hasPerm("blockTrolling")) - (0, commands_1.fail)(f(templateObject_5 || (templateObject_5 = __makeTemplateObject(["Operation aborted: Player ", " is insufficiently trollable."], ["Operation aborted: Player ", " is insufficiently trollable."])), args.player)); + (0, commands_1.fail)(f(templateObject_6 || (templateObject_6 = __makeTemplateObject(["Operation aborted: Player ", " is insufficiently trollable."], ["Operation aborted: Player ", " is insufficiently trollable."])), args.player)); var oldName = args.player.name; args.player.player.name = args.player.prefixedName = args.newname; args.player.shouldUpdateName = false; @@ -575,10 +588,10 @@ exports.commands = (0, commands_1.consoleCommandList)({ if (args.player.marked()) { //overload: overwrite stoptime if (!args.time) - (0, commands_1.fail)(f(templateObject_6 || (templateObject_6 = __makeTemplateObject(["Player ", " is already marked."], ["Player ", " is already marked."])), args.player)); + (0, commands_1.fail)(f(templateObject_7 || (templateObject_7 = __makeTemplateObject(["Player ", " is already marked."], ["Player ", " is already marked."])), args.player)); var previousTime = (0, utils_1.formatTime)(args.player.unmarkTime - Date.now()); args.player.updateStopTime(args.time); - outputSuccess(f(templateObject_7 || (templateObject_7 = __makeTemplateObject(["Player ", "'s stop time has been updated to ", " (was ", ")."], ["Player ", "'s stop time has been updated to ", " (was ", ")."])), args.player, (0, utils_1.formatTime)(args.time), previousTime)); + outputSuccess(f(templateObject_8 || (templateObject_8 = __makeTemplateObject(["Player ", "'s stop time has been updated to ", " (was ", ")."], ["Player ", "'s stop time has been updated to ", " (was ", ")."])), args.player, (0, utils_1.formatTime)(args.time), previousTime)); return; } var time = (_b = args.time) !== null && _b !== void 0 ? _b : 604800000; @@ -757,4 +770,4 @@ exports.commands = (0, commands_1.consoleCommandList)({ }, }, }); -var templateObject_1, templateObject_2, templateObject_3, templateObject_4, templateObject_5, templateObject_6, templateObject_7; +var templateObject_1, templateObject_2, templateObject_3, templateObject_4, templateObject_5, templateObject_6, templateObject_7, templateObject_8; diff --git a/build/scripts/funcs.js b/build/scripts/funcs.js index 0d6926dc..b9f08b54 100644 --- a/build/scripts/funcs.js +++ b/build/scripts/funcs.js @@ -50,6 +50,7 @@ exports.parseError = parseError; exports.tagProcessor = tagProcessor; exports.tagProcessorPartial = tagProcessorPartial; exports.random = random; +exports.getIPAddress = getIPAddress; var storedValues = {}; /** * Stores the output of a function and returns that value @@ -357,3 +358,12 @@ function random(arg0, arg1) { return arg0[Math.floor(Math.random() * arg0.length)]; } } +function getIPAddress(fallback) { + var _a, _b, _c; + if (fallback === void 0) { fallback = "127.0.0.1"; } + return (_c = (_b = (_a = Packages.java.util.Collections.list(Packages.java.net.NetworkInterface.getNetworkInterfaces()) + .stream() + .filter(function (i) { return i.isUp() && !i.isLoopback(); }) + .findFirst() + .orElse(null)) === null || _a === void 0 ? void 0 : _a.getInterfaceAddresses().stream().map(function (s) { return s.getAddress(); }).filter(function (a) { return a instanceof Packages.java.net.Inet4Address; }).findFirst().orElse(null)) === null || _b === void 0 ? void 0 : _b.getHostAddress()) !== null && _c !== void 0 ? _c : fallback; +} diff --git a/build/scripts/main.js b/build/scripts/main.js index fbf1bcff..0162fd35 100644 --- a/build/scripts/main.js +++ b/build/scripts/main.js @@ -1,5 +1,5 @@ /* -Copyright © BalaM314, 2024. All Rights Reserved. +Copyright © BalaM314, 2025. All Rights Reserved. This is a special file which is automatically loaded by the game server. It only contains polyfills, and requires index.js. */ @@ -13,6 +13,10 @@ importPackage(Packages.java.util.regex); importClass(Packages.java.lang.Runtime); importClass(Packages.java.lang.ProcessBuilder); importClass(Packages.java.nio.file.Paths); +importClass(Packages.java.io.ByteArrayOutputStream); +importClass(Packages.java.io.DataOutputStream); +importClass(Packages.java.io.ByteArrayInputStream); +importClass(Packages.java.io.DataInputStream); //Polyfills Object.entries = o => Object.keys(o).map(k => [k, o[k]]); diff --git a/build/scripts/players.js b/build/scripts/players.js index 7a105dfc..b128fb15 100644 --- a/build/scripts/players.js +++ b/build/scripts/players.js @@ -62,8 +62,9 @@ var funcs_4 = require("./funcs"); var funcs_5 = require("./funcs"); var FishPlayer = /** @class */ (function () { function FishPlayer(_a, player) { - var uuid = _a.uuid, name = _a.name, _b = _a.muted, muted = _b === void 0 ? false : _b, _c = _a.autoflagged, autoflagged = _c === void 0 ? false : _c, _d = _a.unmarkTime, unmarked = _d === void 0 ? -1 : _d, _e = _a.highlight, highlight = _e === void 0 ? null : _e, _f = _a.history, history = _f === void 0 ? [] : _f, _g = _a.rainbow, rainbow = _g === void 0 ? null : _g, _h = _a.rank, rank = _h === void 0 ? "player" : _h, _j = _a.flags, flags = _j === void 0 ? [] : _j, usid = _a.usid, _k = _a.chatStrictness, chatStrictness = _k === void 0 ? "chat" : _k, lastJoined = _a.lastJoined, firstJoined = _a.firstJoined, stats = _a.stats, _l = _a.showRankPrefix, showRankPrefix = _l === void 0 ? true : _l; - var _m, _o, _p, _q, _r; + var _b; + var uuid = _a.uuid, name = _a.name, _c = _a.muted, muted = _c === void 0 ? false : _c, _d = _a.autoflagged, autoflagged = _d === void 0 ? false : _d, _e = _a.unmarkTime, unmarked = _e === void 0 ? -1 : _e, _f = _a.highlight, highlight = _f === void 0 ? null : _f, _g = _a.history, history = _g === void 0 ? [] : _g, _h = _a.rainbow, rainbow = _h === void 0 ? null : _h, _j = _a.rank, rank = _j === void 0 ? "player" : _j, _k = _a.flags, flags = _k === void 0 ? [] : _k, usid = _a.usid, _l = _a.chatStrictness, chatStrictness = _l === void 0 ? "chat" : _l, lastJoined = _a.lastJoined, firstJoined = _a.firstJoined, stats = _a.stats, _m = _a.showRankPrefix, showRankPrefix = _m === void 0 ? true : _m; + var _o, _p, _q, _r; //Transients this.player = null; this.pet = ""; @@ -95,23 +96,24 @@ var FishPlayer = /** @class */ (function () { this.lastRatelimitedMessage = -1; this.changedTeam = false; this.ipDetectedVpn = false; + this.approveNextLogin = false; this.chatStrictness = "chat"; - this.uuid = (_m = uuid !== null && uuid !== void 0 ? uuid : player === null || player === void 0 ? void 0 : player.uuid()) !== null && _m !== void 0 ? _m : (0, funcs_3.crash)("Attempted to create FishPlayer with no UUID"); - this.name = (_o = name !== null && name !== void 0 ? name : player === null || player === void 0 ? void 0 : player.name) !== null && _o !== void 0 ? _o : "Unnamed player [ERROR]"; + this.uuid = (_o = uuid !== null && uuid !== void 0 ? uuid : player === null || player === void 0 ? void 0 : player.uuid()) !== null && _o !== void 0 ? _o : (0, funcs_3.crash)("Attempted to create FishPlayer with no UUID"); + this.name = (_p = name !== null && name !== void 0 ? name : player === null || player === void 0 ? void 0 : player.name) !== null && _p !== void 0 ? _p : "Unnamed player [ERROR]"; this.prefixedName = this.name; this.muted = muted; this.unmarkTime = unmarked; this.lastJoined = lastJoined !== null && lastJoined !== void 0 ? lastJoined : -1; - this.firstJoined = (_p = firstJoined !== null && firstJoined !== void 0 ? firstJoined : lastJoined) !== null && _p !== void 0 ? _p : Date.now(); + this.firstJoined = (_q = firstJoined !== null && firstJoined !== void 0 ? firstJoined : lastJoined) !== null && _q !== void 0 ? _q : Date.now(); this.autoflagged = autoflagged; this.highlight = highlight; this.history = history; this.player = player; this.rainbow = rainbow; this.cleanedName = (0, funcs_2.escapeStringColorsServer)(Strings.stripColors(this.name)); - this.rank = (_q = ranks_1.Rank.getByName(rank)) !== null && _q !== void 0 ? _q : ranks_1.Rank.player; + this.rank = (_r = ranks_1.Rank.getByName(rank)) !== null && _r !== void 0 ? _r : ranks_1.Rank.player; this.flags = new Set(flags.map(ranks_1.RoleFlag.getByName).filter(function (f) { return f != null; })); - this.usid = (_r = usid !== null && usid !== void 0 ? usid : player === null || player === void 0 ? void 0 : player.usid()) !== null && _r !== void 0 ? _r : null; + this.usidMapping = typeof usid === "string" ? (_b = {}, _b[config_1.localIPAddress] = usid, _b) : (usid !== null && usid !== void 0 ? usid : {}); this.chatStrictness = chatStrictness; this.stats = stats !== null && stats !== void 0 ? stats : { blocksBroken: 0, @@ -537,10 +539,9 @@ var FishPlayer = /** @class */ (function () { /** Must be called at player join, before updateName(). */ FishPlayer.prototype.updateSavedInfoFromPlayer = function (player) { var _this = this; - var _a; this.player = player; this.name = player.name; - (_a = this.usid) !== null && _a !== void 0 ? _a : (this.usid = player.usid()); + //Do not update USID here this.flags.forEach(function (f) { if (!f.peristent) _this.flags.delete(f); @@ -729,14 +730,37 @@ var FishPlayer = /** @class */ (function () { }; /** Checks if this player's USID is correct. */ FishPlayer.prototype.checkUsid = function () { - if (this.usid != null && this.usid != "" && this.player.usid() != this.usid) { - Log.err("&rUSID mismatch for player &c\"".concat(this.cleanedName, "\"&r: stored usid is &c").concat(this.usid, "&r, but they tried to connect with usid &c").concat(this.player.usid(), "&r")); - if (this.hasPerm("usidCheck")) { - this.kick("Authorization failure!", 1); - FishPlayer.lastAuthKicked = this; + var storedUSID = this.usid(); + var usidMissing = storedUSID == null || storedUSID; + var receivedUSID = this.player.usid(); + if (this.hasPerm("usidCheck")) { + if (usidMissing) { + if (this.hasPerm("admin")) { + //Admin missing USID, don't let them in + Log.err("&rUSID missing for privileged player &c\"".concat(this.cleanedName, "\"&r: no stored usid, cannot authenticate.\nRun &lgapproveauth ").concat(receivedUSID, "&fr if you have verified this connection attempt.")); + this.kick("Authorization failure! Please ask a staff member with Console Access to approve this connection.", 1); + FishPlayer.lastAuthKicked = this; + return false; + } + else { + Log.info("Acquired USID for player &c\"".concat(this.cleanedName, "\"&fr: &c\"").concat(receivedUSID, "\"&fr")); + } + } + else { + if (receivedUSID != storedUSID) { + Log.err("&rUSID mismatch for player &c\"".concat(this.cleanedName, "\"&r: stored usid is &c").concat(storedUSID, "&r, but they tried to connect with usid &c").concat(receivedUSID, "&r\nRun &lgapproveauth ").concat(receivedUSID, "&fr if you have verified this connection attempt.")); + this.kick("Authorization failure!", 1); + FishPlayer.lastAuthKicked = this; + return false; + } + } + } + else { + if (!usidMissing && receivedUSID != storedUSID) { + Log.err("&rUSID mismatch for player &c\"".concat(this.cleanedName, "\"&r: stored usid is &c").concat(storedUSID, "&r, but they tried to connect with usid &c").concat(receivedUSID, "&r")); } - return false; } + this.setUSID(receivedUSID); return true; }; FishPlayer.prototype.displayTrail = function () { @@ -921,7 +945,7 @@ var FishPlayer = /** @class */ (function () { } }; FishPlayer.prototype.write = function (out) { - var _a, _b; + var _a, _b, _c; if (typeof this.unmarkTime === "string") this.unmarkTime = 0; out.writeString(this.uuid, 2); @@ -938,7 +962,7 @@ var FishPlayer = /** @class */ (function () { out.writeNumber((_b = (_a = this.rainbow) === null || _a === void 0 ? void 0 : _a.speed) !== null && _b !== void 0 ? _b : 0, 2); out.writeString(this.rank.name, 2); out.writeArray(Array.from(this.flags).filter(function (f) { return f.peristent; }), function (f, str) { return str.writeString(f.name, 2); }, 2); - out.writeString(this.usid, 2); + out.writeString((_c = this.usid()) !== null && _c !== void 0 ? _c : null, 2); out.writeEnumString(this.chatStrictness, ["chat", "strict"]); out.writeNumber(this.lastJoined, 15); out.writeNumber(this.firstJoined, 15); @@ -1126,6 +1150,12 @@ var FishPlayer = /** @class */ (function () { FishPlayer.prototype.info = function () { return Vars.netServer.admins.getInfo(this.uuid); }; + FishPlayer.prototype.usid = function () { + return this.usidMapping[config_1.localIPAddress]; + }; + FishPlayer.prototype.setUSID = function (usid) { + this.usidMapping[config_1.localIPAddress] = usid; + }; /** * Sends this player a chat message. * @param ratelimit Time in milliseconds before sending another ratelimited message. @@ -1139,6 +1169,10 @@ var FishPlayer = /** @class */ (function () { } }; FishPlayer.prototype.setRank = function (rank) { + if (typeof rank === "string") { + rank; + (0, funcs_3.crash)("Type error in FishPlayer.setFlag(): rank is invalid"); + } if (rank == ranks_1.Rank.pi && !config_1.Mode.localDebug) throw new TypeError("Cannot find function setRank in object [object Object]."); this.rank = rank; diff --git a/build/scripts/staffCommands.js b/build/scripts/staffCommands.js index 4b931286..08d7f7e5 100644 --- a/build/scripts/staffCommands.js +++ b/build/scripts/staffCommands.js @@ -892,7 +892,7 @@ exports.commands = (0, commands_1.commandList)({ var javascript = _a.args.javascript, output = _a.output, outputFail = _a.outputFail, sender = _a.sender; //Additional validation couldn't hurt... var playerInfo_AdminUsid = sender.info().adminUsid; - if (!playerInfo_AdminUsid || playerInfo_AdminUsid != sender.player.usid() || sender.usid != sender.player.usid()) { + if (!playerInfo_AdminUsid || playerInfo_AdminUsid != sender.player.usid() || sender.usid() != sender.player.usid()) { api.sendModerationMessage("# !!!!! /js authentication failed !!!!!\nServer: ".concat(config_1.Gamemode.name(), " Player: ").concat((0, funcs_3.escapeTextDiscord)(sender.cleanedName), "/`").concat(sender.uuid, "`\n<@!709904412033810533>")); (0, commands_1.fail)("Authentication failure"); } @@ -934,7 +934,7 @@ exports.commands = (0, commands_1.commandList)({ var javascript = _a.args.javascript, output = _a.output, outputFail = _a.outputFail, sender = _a.sender; //Additional validation couldn't hurt... var playerInfo_AdminUsid = sender.info().adminUsid; - if (!playerInfo_AdminUsid || playerInfo_AdminUsid != sender.player.usid() || sender.usid != sender.player.usid()) { + if (!playerInfo_AdminUsid || playerInfo_AdminUsid != sender.player.usid() || sender.usid() != sender.player.usid()) { api.sendModerationMessage("# !!!!! /js authentication failed !!!!!\nServer: ".concat(config_1.Gamemode.name(), " Player: ").concat((0, funcs_3.escapeTextDiscord)(sender.cleanedName), "/`").concat(sender.uuid, "`\n<@!709904412033810533>")); (0, commands_1.fail)("Authentication failure"); } diff --git a/spec/src/env.ts b/spec/src/env.ts index 805b4345..1568c3d0 100644 --- a/spec/src/env.ts +++ b/spec/src/env.ts @@ -54,4 +54,75 @@ class Fi { } } -Object.assign(globalThis, {Pattern, ObjectIntMap, Seq, Fi}); +const Collections = { + list(e:Enumeration){ + return new ArrayList(e.items); + } +}; +class Enumeration { + constructor(public items:T[]){} +} +class ArrayList { + constructor(public items:T[]){} + stream(){ + return new Stream(this.items); + } +} +class NetworkInterface { + constructor( + public interfaceAddresses: InterfaceAddress[], + public loopback = false, + public up = true, + ){} + getInterfaceAddresses(){ return new ArrayList(this.interfaceAddresses); } + isUp(){ return this.up; } + isLoopback(){ return this.loopback; } + static getNetworkInterfaces(){ + return new Enumeration([ + new NetworkInterface([new InterfaceAddress(new Inet6Address("0:0:0:0:0:0:0:1")), new InterfaceAddress(new Inet4Address("127.0.0.1"))], true), //loopback + new NetworkInterface([new InterfaceAddress(new Inet6Address("fe80:0:0:0:216:3eff:feaa:b35c")), new InterfaceAddress(new Inet4Address("1.2.3.4"))]), //eth0 + ]); + } +} +class Stream { + iterator: IteratorObject; + constructor(items:T[]){ + this.iterator = items.values(); + } + map(operation:(item:T) => U){ + (this as never as Stream).iterator = this.iterator.map(operation); + return this as never as Stream; + } + filter(operation:(item:T) => boolean){ + this.iterator = this.iterator.filter(operation); + return this; + } + findFirst(){ + return new Optional(this.iterator.next()?.value ?? null); + } +} +class Optional { + constructor(public item:T | null){} + orElse(value:U){ + return this.item ?? value; + } +} +class InterfaceAddress { + constructor(public address: InetAddress){} + getAddress(){ return this.address; } +} +class InetAddress { + constructor(public hostAddress: string){} + getHostAddress(){ return this.hostAddress; } +} +class Inet4Address extends InetAddress {} +class Inet6Address extends InetAddress {} + +const Packages = { + java: { + net: { NetworkInterface, Inet4Address }, + util: { Collections } + } +}; + +Object.assign(globalThis, {Pattern, ObjectIntMap, Seq, Fi, Packages}); diff --git a/spec/src/utils.spec.ts b/spec/src/utils.spec.ts index fd8a908c..38616f31 100644 --- a/spec/src/utils.spec.ts +++ b/spec/src/utils.spec.ts @@ -1,4 +1,4 @@ -import { capitalizeText } from "../../build/scripts/funcs.js"; +import { capitalizeText, getIPAddress } from "../../build/scripts/funcs.js"; import { formatTime } from "../../build/scripts/utils.js"; import { maxTime } from "../../build/scripts/globals.js"; @@ -14,6 +14,12 @@ describe("capitalizeText", () => { }); }); +describe("getIPAddress", () => { + it("should return the correct address from the available network interfaces", () => { + expect(getIPAddress()).toEqual("1.2.3.4"); + }); +}); + describe("formatTime", () => { it("should work for normal times", () => { expect(formatTime(1_000)).toEqual("1 second"); diff --git a/spec/tsconfig.json b/spec/tsconfig.json index 8f4aab77..e1433280 100644 --- a/spec/tsconfig.json +++ b/spec/tsconfig.json @@ -6,6 +6,7 @@ "types": ["node", "jasmine"], "composite": false, "declaration": false, + "lib": ["ESNext"], }, "references": [{"path": ".."}], "include": [ diff --git a/src/config.ts b/src/config.ts index f84ede4a..89bb451e 100644 --- a/src/config.ts +++ b/src/config.ts @@ -6,7 +6,7 @@ This file contains configurable constants. import type { PermType } from "./commands"; import { ipPattern, ipPortPattern, uuidPattern } from "./globals"; import { Rank } from "./ranks"; -import { random } from './funcs'; +import { getIPAddress, random } from './funcs'; @@ -348,3 +348,5 @@ export const rules = [ `Failure to follow these rules will result in consequences: likely a Marked Griefer tag for any game disruption, mute for broken chat rules, and bans for repeated offenses or bypasses.` ].map(r => `[white]${r}`); //#endregion + +export const localIPAddress = getIPAddress(); diff --git a/src/consoleCommands.ts b/src/consoleCommands.ts index 9ec9bade..cf18732c 100644 --- a/src/consoleCommands.ts +++ b/src/consoleCommands.ts @@ -5,7 +5,7 @@ This file contains all the console commands, which can be run through the server import * as api from "./api"; import { consoleCommandList, fail } from "./commands"; -import { Mode } from "./config"; +import { localIPAddress, Mode } from "./config"; import * as globals from "./globals"; import { Gamemode, mapRepoURLs } from "./config"; import { maxTime } from "./globals"; @@ -71,7 +71,7 @@ export const commands = consoleCommandList({ all IPs used: ${playerInfo.ips.map((n:string) => (n == playerInfo.lastIP ? '&c' : '&w') + n + '&fr').items.join(", ")} joined &c${playerInfo.timesJoined}&fr times, kicked &c${playerInfo.timesKicked}&fr times` + (fishP ? ` - USID: &c${fishP.usid}&fr + USID: &c${fishP.usid()}&fr Rank: &c${fishP.rank.name}&fr Marked: ${fishP.marked() ? `&runtil ${formatTimeRelative(fishP.unmarkTime)}` : fishP.autoflagged ? "&rautoflagged" : "&gfalse"}&fr Muted: &c${f.boolBad(fishP.muted)}&fr` @@ -93,7 +93,7 @@ export const commands = consoleCommandList({ outputString.push( `Info for player &c"${player.cleanedName}" &lk(${player.name})&fr UUID: &c"${playerInfo.id}"&fr - USID: &c${player.usid ? `"${player.usid}"` : "unknown"}&fr + USID: &c${player.usid() ? `"${player.usid()}"` : "unknown"}&fr all names used: ${playerInfo.names.map((n:string) => `&c"${n}"&fr`).items.join(', ')} all IPs used: ${playerInfo.ips.map((n:string) => (n == playerInfo.lastIP ? '&c' : '&w') + n + '&fr').items.join(", ")} joined &c${playerInfo.timesJoined}&fr times, kicked &c${playerInfo.timesKicked}&fr times @@ -332,7 +332,7 @@ export const commands = consoleCommandList({ let total = 0; for(const [uuid, fishP] of Object.entries(FishPlayer.cachedPlayers)){ total ++; - fishP.usid = null; + fishP.setUSID(undefined); } FishPlayer.saveAll(); output(`Removed ${total} stored USIDs.`); @@ -352,11 +352,21 @@ export const commands = consoleCommandList({ ? `Player ${args.player} has joined the server, but their info was not cached, most likely because they have no rank, so there is no stored USID.` : `Unknown player ${args.player}` ); - const oldusid = player.usid; - player.usid = null; + if(player.ranksAtLeast("admin")) fail(`Please use the approveauth command instead.`); + const oldusid = player.usid(); + player.setUSID(undefined); outputSuccess(`Removed the usid of player ${player.name}/${player.uuid} (was ${oldusid})`); } }, + approveauth: { + args: ["usid:string"], + description: `Sets the USID of a player.`, + handler({args, outputSuccess, f}){ + const player = FishPlayer.lastAuthKicked ?? fail(`No authorization failures have occurred since the last restart.`); + player.setUSID(args.usid); + outputSuccess(f`Set USID for player ${player} to ${args.usid}.`); + } + }, update: { args: ["branch:string?"], description: "Updates the plugin.", diff --git a/src/funcs.ts b/src/funcs.ts index 39a97953..95f516f6 100644 --- a/src/funcs.ts +++ b/src/funcs.ts @@ -276,3 +276,20 @@ export function random(arg0: unknown, arg1?: number): any { } } +export function getIPAddress(fallback:string = "127.0.0.1"):string { + return Packages.java.util.Collections.list( + Packages.java.net.NetworkInterface.getNetworkInterfaces() + ) + .stream() + .filter((i:any) => i.isUp() && !i.isLoopback()) + .findFirst() + .orElse(null) + ?.getInterfaceAddresses() + .stream() + .map((s:any) => s.getAddress()) + .filter((a:any) => a instanceof Packages.java.net.Inet4Address) + .findFirst() + .orElse(null) + ?.getHostAddress() ?? fallback; +} + diff --git a/src/players.ts b/src/players.ts index cae5a3ef..f27ee15c 100644 --- a/src/players.ts +++ b/src/players.ts @@ -6,7 +6,7 @@ This file contains the FishPlayer class, and many player-related functions. import * as api from "./api"; import { Perm, PermType } from "./commands"; import * as globals from "./globals"; -import { FColor, Gamemode, heuristics, Mode, prefixes, rules, stopAntiEvadeTime, text, tips } from "./config"; +import { FColor, Gamemode, heuristics, localIPAddress, Mode, prefixes, rules, stopAntiEvadeTime, text, tips } from "./config"; import { uuidPattern } from "./globals"; import { Menu } from "./menus"; import { Rank, RankName, RoleFlag, RoleFlagName } from "./ranks"; @@ -92,6 +92,7 @@ export class FishPlayer { lastRatelimitedMessage = -1; changedTeam = false; ipDetectedVpn = false; + approveNextLogin = false; //Stored data uuid: string; @@ -106,7 +107,7 @@ export class FishPlayer { speed: number; } | null; history: PlayerHistoryEntry[]; - usid: string | null; + usidMapping: Partial>; chatStrictness: "chat" | "strict" = "chat"; /** -1 represents unknown */ lastJoined:number; @@ -142,7 +143,7 @@ export class FishPlayer { this.cleanedName = escapeStringColorsServer(Strings.stripColors(this.name)); this.rank = Rank.getByName(rank) ?? Rank.player; this.flags = new Set(flags.map(RoleFlag.getByName).filter((f):f is RoleFlag => f != null)); - this.usid = usid ?? player?.usid() ?? null; + this.usidMapping = typeof usid === "string" ? {[localIPAddress]: usid} : (usid ?? {}); this.chatStrictness = chatStrictness; this.stats = stats ?? { blocksBroken: 0, @@ -507,7 +508,7 @@ export class FishPlayer { updateSavedInfoFromPlayer(player:mindustryPlayer){ this.player = player; this.name = player.name; - this.usid ??= player.usid(); + //Do not update USID here this.flags.forEach(f => { if(!f.peristent) this.flags.delete(f); }); @@ -670,14 +671,34 @@ If you are unable to change it, please download Mindustry from Steam or itch.io. } /** Checks if this player's USID is correct. */ checkUsid(){ - if(this.usid != null && this.usid != "" && this.player!.usid() != this.usid){ - Log.err(`&rUSID mismatch for player &c"${this.cleanedName}"&r: stored usid is &c${this.usid}&r, but they tried to connect with usid &c${this.player!.usid()}&r`); - if(this.hasPerm("usidCheck")){ - this.kick(`Authorization failure!`, 1); - FishPlayer.lastAuthKicked = this; + const storedUSID = this.usid(); + const usidMissing = storedUSID == null || storedUSID; + const receivedUSID = this.player!.usid(); + if(this.hasPerm("usidCheck")){ + if(usidMissing){ + if(this.hasPerm("admin")){ + //Admin missing USID, don't let them in + Log.err(`&rUSID missing for privileged player &c"${this.cleanedName}"&r: no stored usid, cannot authenticate.\nRun &lgapproveauth ${receivedUSID}&fr if you have verified this connection attempt.`); + this.kick(`Authorization failure! Please ask a staff member with Console Access to approve this connection.`, 1); + FishPlayer.lastAuthKicked = this; + return false; + } else { + Log.info(`Acquired USID for player &c"${this.cleanedName}"&fr: &c"${receivedUSID}"&fr`); + } + } else { + if(receivedUSID != storedUSID){ + Log.err(`&rUSID mismatch for player &c"${this.cleanedName}"&r: stored usid is &c${storedUSID}&r, but they tried to connect with usid &c${receivedUSID}&r\nRun &lgapproveauth ${receivedUSID}&fr if you have verified this connection attempt.`); + this.kick(`Authorization failure!`, 1); + FishPlayer.lastAuthKicked = this; + return false; + } + } + } else { + if(!usidMissing && receivedUSID != storedUSID){ + Log.err(`&rUSID mismatch for player &c"${this.cleanedName}"&r: stored usid is &c${storedUSID}&r, but they tried to connect with usid &c${receivedUSID}&r`); } - return false; } + this.setUSID(receivedUSID); return true; } displayTrail(){ @@ -858,7 +879,7 @@ We apologize for the inconvenience.` out.writeNumber(this.rainbow?.speed ?? 0, 2); out.writeString(this.rank.name, 2); out.writeArray(Array.from(this.flags).filter(f => f.peristent), (f, str) => str.writeString(f.name, 2), 2); - out.writeString(this.usid, 2); + out.writeString(this.usid() ?? null, 2); out.writeEnumString(this.chatStrictness, ["chat", "strict"]); out.writeNumber(this.lastJoined, 15); out.writeNumber(this.firstJoined, 15); @@ -1019,6 +1040,12 @@ We apologize for the inconvenience.` info():PlayerInfo { return Vars.netServer.admins.getInfo(this.uuid); } + usid():string | undefined { + return this.usidMapping[localIPAddress]; + } + setUSID(usid:string | undefined){ + this.usidMapping[localIPAddress] = usid; + } /** * Sends this player a chat message. * @param ratelimit Time in milliseconds before sending another ratelimited message. @@ -1031,6 +1058,10 @@ We apologize for the inconvenience.` } setRank(rank:Rank){ + if(typeof rank === "string"){ + rank satisfies never; + crash(`Type error in FishPlayer.setFlag(): rank is invalid`); + } if(rank == Rank.pi && !Mode.localDebug) throw new TypeError(`Cannot find function setRank in object [object Object].`); this.rank = rank; this.updateName(); diff --git a/src/staffCommands.ts b/src/staffCommands.ts index b7fea29f..b23e98ad 100644 --- a/src/staffCommands.ts +++ b/src/staffCommands.ts @@ -5,7 +5,7 @@ This file contains the in-game chat commands that can be run by trusted staff. import * as api from "./api"; import { Perm, Req, command, commandList, fail } from "./commands"; -import { Gamemode, Mode, rules, stopAntiEvadeTime } from "./config"; +import { Gamemode, localIPAddress, Mode, rules, stopAntiEvadeTime } from "./config"; import { maxTime } from "./globals"; import { updateMaps } from "./files"; import * as fjsContext from "./fjsContext"; @@ -724,7 +724,7 @@ export const commands = commandList({ //Additional validation couldn't hurt... const playerInfo_AdminUsid = sender.info().adminUsid; - if(!playerInfo_AdminUsid || playerInfo_AdminUsid != sender.player!.usid() || sender.usid != sender.player!.usid()){ + if(!playerInfo_AdminUsid || playerInfo_AdminUsid != sender.player!.usid() || sender.usid() != sender.player!.usid()){ api.sendModerationMessage( `# !!!!! /js authentication failed !!!!! Server: ${Gamemode.name()} Player: ${escapeTextDiscord(sender.cleanedName)}/\`${sender.uuid}\` @@ -765,7 +765,7 @@ Server: ${Gamemode.name()} Player: ${escapeTextDiscord(sender.cleanedName)}/\`${ //Additional validation couldn't hurt... const playerInfo_AdminUsid = sender.info().adminUsid; - if(!playerInfo_AdminUsid || playerInfo_AdminUsid != sender.player!.usid() || sender.usid != sender.player!.usid()){ + if(!playerInfo_AdminUsid || playerInfo_AdminUsid != sender.player!.usid() || sender.usid() != sender.player!.usid()){ api.sendModerationMessage( `# !!!!! /js authentication failed !!!!! Server: ${Gamemode.name()} Player: ${escapeTextDiscord(sender.cleanedName)}/\`${sender.uuid}\` diff --git a/src/types.ts b/src/types.ts index 6cecb9e4..b4b7207c 100644 --- a/src/types.ts +++ b/src/types.ts @@ -232,7 +232,7 @@ export interface FishPlayerData { highlight: string | null; rainbow: { speed:number; } | null; history: PlayerHistoryEntry[]; - usid: string | null; + usid: string | Partial> | null; chatStrictness: "chat" | "strict"; lastJoined: number; firstJoined: number;