Skip to content
Merged
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
42 changes: 41 additions & 1 deletion src/chat/chat.service.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@ describe("ChatService direct messages", () => {
eval: jest.fn().mockResolvedValue([1, 1]),
};

const rcon = { connect: jest.fn(), send: jest.fn() };

let service: ChatService;
let acceptedFriendships: Array<[string, string]>;
let role: string;
Expand Down Expand Up @@ -153,6 +155,15 @@ describe("ChatService direct messages", () => {
};
}

if (query.matches_by_pk?.server) {
return {
matches_by_pk: {
status: "Live",
server: { id: "server-1", plugin_runtime: "counterstrikesharp" },
},
};
}

if (query.matches_by_pk) {
return myMatches.includes(query.matches_by_pk.__args.id)
? {
Expand Down Expand Up @@ -215,9 +226,12 @@ describe("ChatService direct messages", () => {
staff = [];
role = "user";
queries = [];
rcon.send.mockResolvedValue(undefined);
rcon.connect.mockResolvedValue(rcon);

service = new ChatService(
logger as any,
{ } as any,
{ connect: rcon.connect } as any,
hasuraService as any,
postgres as any,
{ getConnection: () => redis } as any,
Expand Down Expand Up @@ -388,6 +402,32 @@ describe("ChatService direct messages", () => {
});
});

// Match chat is relayed into the game server as an rcon command with the
// message inlined in quotes, so what a player types has to be unable to
// terminate that argument or that line.
describe("relaying to the game server", () => {
const relayed = async (message: string) => {
await service.sendChatToServer("m-1", message);
return rcon.send.mock.calls.at(-1)?.[0] as string;
};

it("sends the message as one quoted argument", async () => {
expect(await relayed("nice shot")).toBe('css_web_chat "nice shot"');
});

it("flattens a multi line message onto one line", async () => {
expect(await relayed("top\nbottom")).toBe('css_web_chat "top bottom"');
expect(await relayed("top\r\nbottom")).toBe('css_web_chat "top bottom"');
});

it("strips quotes so the message cannot escape the argument", async () => {
expect(await relayed('x" ; quit ; say "')).not.toContain('"x"');
expect(await relayed('x" ; quit ; say "')).toBe(
'css_web_chat "x ; quit ; say"',
);
});
});

describe("rosters", () => {
it("resolves both parties of a conversation", async () => {
expect(
Expand Down
16 changes: 15 additions & 1 deletion src/chat/chat.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1417,6 +1417,18 @@ export class ChatService {
}
}

// The message is inlined into an rcon command inside quotes, so a quote ends
// the argument and a newline ends the command: either one turns the rest of
// what a player typed into console input. Chat in game is one line anyway.
private static oneRconArgument(message: string) {
return message
.replace(/[\r\n]+/g, " ")
.replace(/"/g, "")
// eslint-disable-next-line no-control-regex
.replace(/[\x00-\x1f]/g, "")
.trim();
}

public async sendChatToServer(matchId: string, message: string) {
try {
const { matches_by_pk } = await this.hasuraService.query({
Expand Down Expand Up @@ -1452,7 +1464,9 @@ export class ChatService {
? "css_web_chat"
: "sw_web_chat";

return await rcon.send(`${command} "${message}"`);
return await rcon.send(
`${command} "${ChatService.oneRconArgument(message)}"`,
);
} catch (error) {
this.logger.warn(
`[${matchId}] unable to send match to server`,
Expand Down
Loading