Skip to content

Commit

Permalink
feat(games): use logsecret to match game events (#1161)
Browse files Browse the repository at this point in the history
  • Loading branch information
garrappachc authored Aug 12, 2021
1 parent 7303ddc commit 7382df3
Show file tree
Hide file tree
Showing 24 changed files with 369 additions and 325 deletions.
22 changes: 22 additions & 0 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
{
"cSpell.words": [
"Configurator",
"Cooldown",
"Logsecret",
"Logsecrets",
"Microtasks",
"demoman",
"ma",
"mały",
"mongod",
"nestjs",
"patreon",
"pyro",
"rcon",
"snakewater",
"tftrue",
"typegoose",
"upsert",
"y"
]
}
17 changes: 0 additions & 17 deletions package-lock.json

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

1 change: 0 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,6 @@
"reflect-metadata": "0.1.13",
"rimraf": "3.0.2",
"rxjs": "7.3.0",
"srcds-log-receiver": "1.0.2",
"steamid": "2.0.0"
},
"devDependencies": {
Expand Down
28 changes: 20 additions & 8 deletions src/game-servers/diagnostic-checks/log-forwarding.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,16 @@
import { Environment } from '@/environment/environment';
import { logAddressAdd, logAddressDel } from '@/games/utils/rcon-commands';
import {
logAddressAdd,
logAddressDel,
svLogsecret,
} from '@/games/utils/rcon-commands';
import { LogReceiverService } from '@/log-receiver/services/log-receiver.service';
import { Injectable, Scope } from '@nestjs/common';
import { generate } from 'generate-password';
import { Rcon } from 'rcon-client/lib';
import { LogMessage, LogReceiver } from 'srcds-log-receiver';
import { DiagnosticCheckResult } from '../interfaces/diagnostic-check-result';
import { DiagnosticCheckRunner } from '../interfaces/diagnostic-check-runner';
import { generateLogsecret } from '../utils/generate-logsecret';

const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));

Expand All @@ -15,7 +20,7 @@ export class LogForwarding implements DiagnosticCheckRunner {
critical = true;

constructor(
private logReceiver: LogReceiver,
private logReceiverService: LogReceiverService,
private environment: Environment,
) {}

Expand All @@ -30,21 +35,25 @@ export class LogForwarding implements DiagnosticCheckRunner {
}

return new Promise((resolve) => {
const logSecret = generateLogsecret();
const secret = generate({ length: 32, numbers: true });
const timer = setTimeout(
() =>
resolve({
success: false,
reportedErrors: [
`No logs received over the UDP protocol on port ${this.logReceiver.opts.port}. Check your firewall settings.`,
`No logs received over the UDP protocol on port ${this.environment.logRelayPort}. Check your firewall settings.`,
],
reportedWarnings: [],
}),
5000,
);

this.logReceiver.on('data', (data: LogMessage) => {
if (new RegExp(`Console.+say\\s"${secret}"$`).test(data.message)) {
const subscription = this.logReceiverService.data.subscribe((data) => {
if (
data.password === logSecret &&
new RegExp(`Console.+say\\s"${secret}"$`).test(data.payload)
) {
clearTimeout(timer);
resolve({
success: true,
Expand All @@ -56,10 +65,13 @@ export class LogForwarding implements DiagnosticCheckRunner {

const logAddress = `${this.environment.logRelayAddress}:${this.environment.logRelayPort}`;
rcon
.send(logAddressAdd(logAddress))
.send(svLogsecret(logSecret))
.then(() => rcon.send(logAddressAdd(logAddress)))
.then(() => sleep(1000))
.then(() => rcon.send(`say ${secret}`))
.then(() => rcon.send(logAddressDel(logAddress)));
.then(() => rcon.send(logAddressDel(logAddress)))
.then(() => rcon.send(svLogsecret()))
.then(() => subscription.unsubscribe());
});
}
}
3 changes: 0 additions & 3 deletions src/game-servers/models/game-server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,9 +31,6 @@ export class GameServer extends MongooseDocument {
@Prop({ default: false })
isOnline?: boolean; // was the server online last we checked

@Prop({ type: () => [String], index: true })
resolvedIpAddresses?: string[]; // for tracing game server logs

@Prop()
mumbleChannelName?: string;

Expand Down
32 changes: 0 additions & 32 deletions src/game-servers/services/game-servers.service.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -142,33 +142,11 @@ describe('GameServersService', () => {
});
});

it('should resolve ip addresses', () => {
expect(gameServer.resolvedIpAddresses).toEqual(['1.2.3.4']);
});

it('should save the mumble channel', () => {
expect(gameServer.mumbleChannelName).toEqual('some mumble channel');
});
});

describe('when the address is a raw IP address', () => {
let gameServer: GameServer;

beforeEach(async () => {
gameServer = await service.addGameServer({
name: 'test game server',
address: '151.80.108.144',
port: '27017',
rconPassword: 'test rcon password',
mumbleChannelName: 'some mumble channel name',
});
});

it('should store the ip address', () => {
expect(gameServer.resolvedIpAddresses).toEqual(['151.80.108.144']);
});
});

it('should emit the gameServerAdded event', async () =>
new Promise<void>((resolve) => {
events.gameServerAdded.subscribe(({ gameServer, adminId }) => {
Expand Down Expand Up @@ -315,16 +293,6 @@ describe('GameServersService', () => {
});
});

describe('#getGameServerByEventSource()', () => {
it('should return the correct server', async () => {
const server = await service.getGameServerByEventSource({
address: '127.0.0.1',
port: 27015,
});
expect(server.id).toEqual(testGameServer.id);
});
});

describe('#checkAllServers()', () => {
it('should check whether every server is online', async () => {
const spy = jest.spyOn(isServerOnline, 'isServerOnline');
Expand Down
37 changes: 0 additions & 37 deletions src/game-servers/services/game-servers.service.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
import { Injectable, Logger } from '@nestjs/common';
import { GameServer, GameServerDocument } from '../models/game-server';
import { resolve as resolveCb } from 'dns';
import { promisify } from 'util';
import { isServerOnline } from '../utils/is-server-online';
import { Cron, CronExpression } from '@nestjs/schedule';
import { Mutex } from 'async-mutex';
Expand All @@ -11,8 +9,6 @@ import { plainToClass } from 'class-transformer';
import { InjectModel } from '@nestjs/mongoose';
import { Model, Error } from 'mongoose';

const resolve = promisify(resolveCb);

@Injectable()
export class GameServersService {
private readonly logger = new Logger(GameServersService.name);
Expand Down Expand Up @@ -42,21 +38,6 @@ export class GameServersService {
params: GameServer,
adminId?: string,
): Promise<GameServer> {
if (
/^(?!0)(?!.*\.$)((1?\d?\d|25[0-5]|2[0-4]\d)(\.|$)){4}$/.test(
params.address,
)
) {
// raw ip address was given
params.resolvedIpAddresses = [params.address];
} else {
const resolvedIpAddresses = await resolve(params.address);
this.logger.verbose(
`resolved addresses for ${params.address}: ${resolvedIpAddresses}`,
);
params.resolvedIpAddresses = resolvedIpAddresses;
}

if (!params.mumbleChannelName) {
const latestServer = await this.gameServerModel
.findOne({ mumbleChannelName: { $ne: null }, deleted: false })
Expand Down Expand Up @@ -151,24 +132,6 @@ export class GameServersService {
return gameServer;
}

async getGameServerByEventSource(eventSource: {
address: string;
port: number;
}): Promise<GameServer> {
return plainToClass(
GameServer,
await this.gameServerModel
.findOne({
deleted: false,
resolvedIpAddresses: eventSource.address,
port: `${eventSource.port}`,
})
.orFail()
.lean()
.exec(),
);
}

@Cron(CronExpression.EVERY_MINUTE)
async checkAllServers() {
this.logger.debug('checking all servers...');
Expand Down
13 changes: 13 additions & 0 deletions src/game-servers/utils/generate-logsecret.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { generate } from 'generate-password';

/**
* Generate string that will be used with the sv_logsecret command.
*/
export const generateLogsecret = () =>
generate({
length: 16,
numbers: true,
symbols: false,
lowercase: false,
uppercase: false,
});
7 changes: 7 additions & 0 deletions src/games/models/game.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,10 @@ import { GameState } from './game-state';
if (ret.assignedSkills) {
delete ret.assignedSkills;
}

if (ret.logSecret) {
delete ret.logSecret;
}
},
},
})
Expand Down Expand Up @@ -78,6 +82,9 @@ export class Game {
@Prop()
stvConnectString?: string;

@Prop({ unique: true, sparse: true })
logSecret?: string;

findPlayerSlot: (playerId: string) => GameSlot;
activeSlots: () => GameSlot[];
}
Expand Down
4 changes: 4 additions & 0 deletions src/games/services/__mocks__/games.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,10 @@ export class GamesService {
return await this.gameModel.findById(gameId);
}

async getByLogSecret(logSecret: string): Promise<GameDocument> {
return await this.gameModel.findOne({ logSecret });
}

async update(gameId: string, update: Partial<Game>) {
return await this.gameModel.findByIdAndUpdate(gameId, update, {
new: true,
Expand Down
Loading

0 comments on commit 7382df3

Please sign in to comment.