Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for tailing logs over SFTP #370

Merged
merged 2 commits into from
Jul 31, 2024
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
12 changes: 8 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -65,12 +65,16 @@ The following section of the configuration contains information about your Squad
"rconPassword": "password",
"logReaderMode": "tail",
"logDir": "C:/path/to/squad/log/folder",
"ftp":{
"host": "xxx.xxx.xxx.xxx",
"ftp": {
"port": 21,
"user": "FTP Username",
"password": "FTP Password",
"useListForSize": false
"password": "FTP Password"
},
"sftp": {
"host": "xxx.xxx.xxx.xxx",
"port": 21,
"username": "SFTP Username",
"password": "SFTP Password"
},
"adminLists": [
{
Expand Down
12 changes: 8 additions & 4 deletions config.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,15 @@
"logReaderMode": "tail",
"logDir": "C:/path/to/squad/log/folder",
"ftp": {
"host": "xxx.xxx.xxx.xxx",
"port": 21,
"user": "FTP Username",
"password": "FTP Password",
"useListForSize": false
"password": "FTP Password"
},
"sftp": {
"host": "xxx.xxx.xxx.xxx",
"port": 21,
"username": "SFTP Username",
"password": "SFTP Password"
},
"adminLists": [
{
Expand Down Expand Up @@ -256,4 +260,4 @@
"RCON": "redBright"
}
}
}
}
2 changes: 1 addition & 1 deletion core/id-parser.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ const ID_MATCHER = /\s*(?<name>[^\s:]+)\s*:\s*(?<id>[^\s]+)/g;
// COMMON CONSTANTS

/** All possible IDs that a player can have. */
export const playerIdNames = ["steamID", "eosID"];
export const playerIdNames = ['steamID', 'eosID'];

// PARSING AND ITERATION

Expand Down
4 changes: 4 additions & 0 deletions core/log-parser/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import moment from 'moment';
import Logger from '../logger.js';

import TailLogReader from './log-readers/tail.js';
import SFTPLogReader from './log-readers/sftp.js';
import FTPLogReader from './log-readers/ftp.js';

export default class LogParser extends EventEmitter {
Expand Down Expand Up @@ -35,6 +36,9 @@ export default class LogParser extends EventEmitter {
case 'tail':
this.logReader = new TailLogReader(this.queue.push, options);
break;
case 'sftp':
this.logReader = new SFTPLogReader(this.queue.push, options);
break;
case 'ftp':
this.logReader = new FTPLogReader(this.queue.push, options);
break;
Expand Down
27 changes: 10 additions & 17 deletions core/log-parser/log-readers/ftp.js
Original file line number Diff line number Diff line change
@@ -1,36 +1,29 @@
import path from 'path';
import FTPTail from 'ftp-tail';
import { FTPTail } from 'ftp-tail';

export default class TailLogReader {
constructor(queueLine, options = {}) {
for (const option of ['host', 'user', 'password', 'logDir'])
for (const option of ['ftp', 'logDir'])
if (!(option in options)) throw new Error(`${option} must be specified.`);

this.reader = new FTPTail({
host: options.host,
port: options.port || 21,
user: options.user,
password: options.password,
secure: options.secure || false,
timeout: options.timeout || 2000,
encoding: 'utf8',
verbose: options.verbose,

path: path.join(options.logDir, options.filename),
this.options = options;

this.reader = new FTPTail({
ftp: options.ftp,
fetchInterval: options.fetchInterval || 0,
maxTempFileSize: options.maxTempFileSize || 5 * 1000 * 1000, // 5 MB

useListForSize: options.useListForSize
maxTempFileSize: options.maxTempFileSize || 5 * 1000 * 1000 // 5 MB
});

if (typeof queueLine !== 'function')
throw new Error('queueLine argument must be specified and be a function.');

this.reader.on('line', queueLine);
}

async watch() {
await this.reader.watch();
await this.reader.watch(
path.join(this.options.logDir, this.options.filename).replace(/\\/g, '/')
);
}

async unwatch() {
Expand Down
32 changes: 32 additions & 0 deletions core/log-parser/log-readers/sftp.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import path from 'path';
import { SFTPTail } from 'ftp-tail';

export default class TailLogReader {
constructor(queueLine, options = {}) {
for (const option of ['sftp', 'logDir'])
if (!(option in options)) throw new Error(`${option} must be specified.`);

this.options = options;

this.reader = new SFTPTail({
sftp: options.sftp,
fetchInterval: options.fetchInterval || 0,
maxTempFileSize: options.maxTempFileSize || 5 * 1000 * 1000 // 5 MB
});

if (typeof queueLine !== 'function')
throw new Error('queueLine argument must be specified and be a function.');

this.reader.on('line', queueLine);
}

async watch() {
await this.reader.watch(
path.join(this.options.logDir, this.options.filename).replace(/\\/g, '/')
);
}

async unwatch() {
await this.reader.unwatch();
}
}
2 changes: 1 addition & 1 deletion core/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
"dependencies": {
"async": "^3.2.0",
"chalk": "^4.1.0",
"ftp-tail": "^1.1.1",
"ftp-tail": "^2.1.0",
"moment": "^2.29.1",
"tail": "^2.0.4"
}
Expand Down
59 changes: 32 additions & 27 deletions squad-server/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -178,13 +178,12 @@ export default class SquadServer extends EventEmitter {
}

setupLogParser() {
this.logParser = new LogParser(
Object.assign(this.options.ftp, {
mode: this.options.logReaderMode,
logDir: this.options.logDir,
host: this.options.ftp.host || this.options.host
})
);
this.logParser = new LogParser({
mode: this.options.logReaderMode,
logDir: this.options.logDir,
sftp: this.options.sftp,
ftp: this.options.ftp
});

this.logParser.on('ADMIN_BROADCAST', (data) => {
this.emit('ADMIN_BROADCAST', data);
Expand Down Expand Up @@ -242,8 +241,7 @@ export default class SquadServer extends EventEmitter {

if (data.victim && data.attacker) {
data.teamkill =
data.victim.teamID === data.attacker.teamID &&
data.victim.eosID !== data.attacker.eosID;
data.victim.teamID === data.attacker.teamID && data.victim.eosID !== data.attacker.eosID;
}

delete data.victimName;
Expand All @@ -260,8 +258,7 @@ export default class SquadServer extends EventEmitter {

if (data.victim && data.attacker)
data.teamkill =
data.victim.teamID === data.attacker.teamID &&
data.victim.eosID !== data.attacker.eosID;
data.victim.teamID === data.attacker.teamID && data.victim.eosID !== data.attacker.eosID;

delete data.victimName;
delete data.attackerName;
Expand All @@ -278,8 +275,7 @@ export default class SquadServer extends EventEmitter {

if (data.victim && data.attacker)
data.teamkill =
data.victim.teamID === data.attacker.teamID &&
data.victim.eosID !== data.attacker.eosID;
data.victim.teamID === data.attacker.teamID && data.victim.eosID !== data.attacker.eosID;

delete data.victimName;
delete data.attackerName;
Expand Down Expand Up @@ -363,14 +359,14 @@ export default class SquadServer extends EventEmitter {
* <code>'eosID'</code>). For <code>'anyID'</code> returns both
* steam and eos IDs as is, no remapping applied.
* @returns {string[]}
*//**
*/ /**
* Get every admin that has the permission.
* @overload
* @arg {string} perm - permission to filter with.
* @arg {'player'} type - return players instead of just IDs. Returns
* only admins that are online.
* @returns {Player[]}
*//**
*/ /**
* Get steamIDs of every admin that has the permission. This overload
* exists for compatibility with pre-EOS API and is equivalent to
* <code>getAdminsWithPermisson(perm, type='steamID')</code>.
Expand All @@ -388,18 +384,27 @@ export default class SquadServer extends EventEmitter {
switch (type) {
// 1) if admin is registered with steamID and is online then swap to eosID
// 2) deduplicate output in case same admin was in 2 lists with different IDs
case 'anyID' : return [
...new Set(ret.map((ID) => {
for (const adm of this.players) {
if(isPlayerID(ID, adm)) return adm.eosID;
}
return ID;
}))
];
case 'player' : return anyIDsToPlayers(ret, this.players);
case 'eosID' : {filter = (ID) => ID.match(steamRgx) === null; break;}
case 'steamID': break;
default: throw new Error(`Expected type == 'steamID'|'eosID'|'anyID'|'player', got '${type}'.`);
case 'anyID':
return [
...new Set(
ret.map((ID) => {
for (const adm of this.players) {
if (isPlayerID(ID, adm)) return adm.eosID;
}
return ID;
})
)
];
case 'player':
return anyIDsToPlayers(ret, this.players);
case 'eosID': {
filter = (ID) => ID.match(steamRgx) === null;
break;
}
case 'steamID':
break;
default:
throw new Error(`Expected type == 'steamID'|'eosID'|'anyID'|'player', got '${type}'.`);
}
const matches = [];
const fails = [];
Expand Down
2 changes: 1 addition & 1 deletion squad-server/log-parser/player-damaged.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ export default {
regex:
/^\[([0-9.:-]+)]\[([ 0-9]*)]LogSquad: Player:(.+) ActualDamage=([0-9.]+) from (.+) \(Online IDs:([^|]+)\| Player Controller ID: ([^ ]+)\)caused by ([A-z_0-9-]+)_C/,
onMatch: (args, logParser) => {
if (args[6].includes("INVALID")) return; // bail in case of bad IDs.
if (args[6].includes('INVALID')) return; // bail in case of bad IDs.
const data = {
raw: args[0],
time: args[1],
Expand Down
2 changes: 1 addition & 1 deletion squad-server/log-parser/player-died.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ export default {
regex:
/^\[([0-9.:-]+)]\[([ 0-9]*)]LogSquadTrace: \[DedicatedServer](?:ASQSoldier::)?Die\(\): Player:(.+) KillingDamage=(?:-)*([0-9.]+) from ([A-z_0-9]+) \(Online IDs:([^)|]+)\| Contoller ID: ([\w\d]+)\) caused by ([A-z_0-9-]+)_C/,
onMatch: (args, logParser) => {
if (args[6].includes("INVALID")) return; // bail in case of bad IDs.
if (args[6].includes('INVALID')) return; // bail in case of bad IDs.
const data = {
...logParser.eventStore.session[args[3]],
raw: args[0],
Expand Down
2 changes: 1 addition & 1 deletion squad-server/log-parser/player-un-possess.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ export default {
regex:
/^\[([0-9.:-]+)]\[([ 0-9]*)]LogSquadTrace: \[DedicatedServer](?:ASQPlayerController::)?OnUnPossess\(\): PC=(.+) \(Online IDs:([^)]+)\)/,
onMatch: (args, logParser) => {
if (args[4].includes("INVALID")) return; // bail in case of bad IDs.
if (args[4].includes('INVALID')) return; // bail in case of bad IDs.
const data = {
raw: args[0],
time: args[1],
Expand Down
2 changes: 1 addition & 1 deletion squad-server/log-parser/player-wounded.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ export default {
regex:
/^\[([0-9.:-]+)]\[([ 0-9]*)]LogSquadTrace: \[DedicatedServer](?:ASQSoldier::)?Wound\(\): Player:(.+) KillingDamage=(?:-)*([0-9.]+) from ([A-z_0-9]+) \(Online IDs:([^)|]+)\| Controller ID: ([\w\d]+)\) caused by ([A-z_0-9-]+)_C/,
onMatch: (args, logParser) => {
if (args[6].includes("INVALID")) return; // bail in case of bad IDs.
if (args[6].includes('INVALID')) return; // bail in case of bad IDs.
const data = {
...logParser.eventStore.session[args[3]],
raw: args[0],
Expand Down
9 changes: 7 additions & 2 deletions squad-server/templates/config-template.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,13 @@
"ftp": {
"port": 21,
"user": "FTP Username",
"password": "FTP Password",
"useListForSize": false
"password": "FTP Password"
},
"sftp": {
"host": "xxx.xxx.xxx.xxx",
"port": 21,
"username": "SFTP Username",
"password": "SFTP Password"
},
"adminLists": [
{
Expand Down
11 changes: 8 additions & 3 deletions squad-server/templates/readme-template.md
Original file line number Diff line number Diff line change
Expand Up @@ -65,11 +65,16 @@ The following section of the configuration contains information about your Squad
"rconPassword": "password",
"logReaderMode": "tail",
"logDir": "C:/path/to/squad/log/folder",
"ftp":{
"ftp": {
"port": 21,
"user": "FTP Username",
"password": "FTP Password",
"useListForSize": false
"password": "FTP Password"
},
"sftp": {
"host": "xxx.xxx.xxx.xxx",
"port": 21,
"username": "SFTP Username",
"password": "SFTP Password"
},
"adminLists": [
{
Expand Down
8 changes: 4 additions & 4 deletions squad-server/utils/any-id.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,10 @@ import { playerIdNames } from 'core/id-parser';
* returns {boolean}
*/
export function isPlayerID(anyID, player) {
for (const idName of playerIdNames) {
if (player[idName] === anyID) return true;
}
return false;
for (const idName of playerIdNames) {
if (player[idName] === anyID) return true;
}
return false;
}

/**
Expand Down