Files
factorio-expcluster/exp_reports/instance.ts
T
BastiaanandClaude Opus 5.5 8e7ac3c872 Pick a long bracket the json does not close in rcon commands (#506)
exp_reports, exp_roles and exp_groups wrapped the json they send to Lua in
a fixed [=[ ]=] long bracket. A report reason, role name or group name
containing ]=] closed it early and the rest ran as Lua through /sc. Any
player can make a report, so any player could run code on the server.

Use the next level the json does not contain, like the other plugins do.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-15 17:07:47 +01:00

98 lines
3.3 KiB
TypeScript

import type { InstancePluginContext } from "@clusterio/host";
import * as messages from "./messages.js";
/** Sent by the lua side when a player reports another. */
export type IpcReportCreate = {
player_name: string,
by_player_name: string,
reason: string,
};
/** Sent by the lua side to list the reports for a player, or for everyone. */
export type IpcReportList = {
caller: string,
player_name: string | undefined,
};
/** Sent by the lua side to delete the reports against a player, optionally only those from one player. */
export type IpcReportDelete = {
caller: string,
player_name: string,
by_player_name: string | undefined,
};
/**
* Bridges the lua side and the controller.
*
* Nothing is kept in the lua state, every request goes to the controller and
* the answer is handed back to lua once it arrives. The instance may have
* stopped by then, in which case the answer is dropped.
*/
export default async function (context: InstancePluginContext) {
const { instance } = context;
/** Hand an answer to lua, unless the instance stopped while it was being fetched. */
async function luaSend(receiver: string, json: any) {
if (instance.status !== "running") {
return;
}
const text = JSON.stringify(json);
// Reasons can contain anything, so pick a long bracket level the json does not close
let level = "=";
while (text.includes(`]${level}]`)) {
level += "=";
}
await instance.sendRcon(
`/sc exp_reports.${receiver}(helpers.json_to_table[${level}[${text}]${level}])`, true
);
}
instance.server.handle("exp_reports:create", async (event: IpcReportCreate) => {
try {
const report = await instance.sendTo("controller", new messages.ReportCreateRequest(
event.player_name, event.reason, event.by_player_name,
));
const reports = await instance.sendTo("controller", new messages.ReportListRequest(event.player_name));
await luaSend("receive_created", {
report: report.toJSON(),
reports: reports.map(other => other.toJSON()),
});
} catch (err: any) {
await luaSend("receive_error", { caller: event.by_player_name, message: err.message });
}
});
instance.server.handle("exp_reports:list", async (event: IpcReportList) => {
try {
const reports = await instance.sendTo("controller", new messages.ReportListRequest(event.player_name));
await luaSend("receive_list", {
caller: event.caller,
player_name: event.player_name,
reports: reports.map(report => report.toJSON()),
});
} catch (err: any) {
await luaSend("receive_error", { caller: event.caller, message: err.message });
}
});
instance.server.handle("exp_reports:delete", async (event: IpcReportDelete) => {
try {
let reports = await instance.sendTo("controller", new messages.ReportListRequest(event.player_name));
if (event.by_player_name !== undefined) {
reports = reports.filter(report => report.byPlayerName === event.by_player_name);
}
for (const report of reports) {
await instance.sendTo("controller", new messages.ReportDeleteRequest(report.id));
}
await luaSend("receive_deleted", {
caller: event.caller,
player_name: event.player_name,
by_player_name: event.by_player_name,
count: reports.length,
});
} catch (err: any) {
await luaSend("receive_error", { caller: event.caller, message: err.message });
}
});
}