Skip to content

Commit

Permalink
Merge pull request #370 from Team-Silver-Sphere/sftp-support
Browse files Browse the repository at this point in the history
Add support for tailing logs over SFTP
  • Loading branch information
Thomas-Smyth authored Jul 31, 2024
2 parents 9cc0d58 + dfa17fa commit a419e53
Show file tree
Hide file tree
Showing 15 changed files with 119 additions and 67 deletions.
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

0 comments on commit a419e53

Please sign in to comment.