diff --git a/README.md b/README.md index 1980a71d6..41c9cddcc 100644 --- a/README.md +++ b/README.md @@ -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": [ { diff --git a/config.json b/config.json index 062377840..cee564284 100644 --- a/config.json +++ b/config.json @@ -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": [ { @@ -256,4 +260,4 @@ "RCON": "redBright" } } -} +} \ No newline at end of file diff --git a/core/id-parser.js b/core/id-parser.js index 7ea5dea01..2254452b5 100644 --- a/core/id-parser.js +++ b/core/id-parser.js @@ -3,7 +3,7 @@ const ID_MATCHER = /\s*(?[^\s:]+)\s*:\s*(?[^\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 diff --git a/core/log-parser/index.js b/core/log-parser/index.js index 642f9e1e9..1e8211b98 100644 --- a/core/log-parser/index.js +++ b/core/log-parser/index.js @@ -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 { @@ -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; diff --git a/core/log-parser/log-readers/ftp.js b/core/log-parser/log-readers/ftp.js index 75ecc9fbd..b6bba7b1d 100644 --- a/core/log-parser/log-readers/ftp.js +++ b/core/log-parser/log-readers/ftp.js @@ -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() { diff --git a/core/log-parser/log-readers/sftp.js b/core/log-parser/log-readers/sftp.js new file mode 100644 index 000000000..4054c0618 --- /dev/null +++ b/core/log-parser/log-readers/sftp.js @@ -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(); + } +} diff --git a/core/package.json b/core/package.json index 1fdd41221..8f67e1b14 100644 --- a/core/package.json +++ b/core/package.json @@ -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" } diff --git a/squad-server/index.js b/squad-server/index.js index 4004ee051..49f0ea937 100644 --- a/squad-server/index.js +++ b/squad-server/index.js @@ -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); @@ -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; @@ -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; @@ -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; @@ -363,14 +359,14 @@ export default class SquadServer extends EventEmitter { * 'eosID'). For 'anyID' 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 * getAdminsWithPermisson(perm, type='steamID'). @@ -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 = []; diff --git a/squad-server/log-parser/player-damaged.js b/squad-server/log-parser/player-damaged.js index 9c70fd7c1..892775f10 100644 --- a/squad-server/log-parser/player-damaged.js +++ b/squad-server/log-parser/player-damaged.js @@ -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], diff --git a/squad-server/log-parser/player-died.js b/squad-server/log-parser/player-died.js index 91c68f2d0..5a3c6172f 100644 --- a/squad-server/log-parser/player-died.js +++ b/squad-server/log-parser/player-died.js @@ -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], diff --git a/squad-server/log-parser/player-un-possess.js b/squad-server/log-parser/player-un-possess.js index 4f1e060a3..55d162a33 100644 --- a/squad-server/log-parser/player-un-possess.js +++ b/squad-server/log-parser/player-un-possess.js @@ -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], diff --git a/squad-server/log-parser/player-wounded.js b/squad-server/log-parser/player-wounded.js index 34002f44b..5a1e0a50c 100644 --- a/squad-server/log-parser/player-wounded.js +++ b/squad-server/log-parser/player-wounded.js @@ -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], diff --git a/squad-server/templates/config-template.json b/squad-server/templates/config-template.json index f8f6458b2..6080d98c4 100644 --- a/squad-server/templates/config-template.json +++ b/squad-server/templates/config-template.json @@ -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": [ { diff --git a/squad-server/templates/readme-template.md b/squad-server/templates/readme-template.md index a1531f360..d5b813170 100644 --- a/squad-server/templates/readme-template.md +++ b/squad-server/templates/readme-template.md @@ -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": [ { diff --git a/squad-server/utils/any-id.js b/squad-server/utils/any-id.js index b403547d2..d8d0e5b0e 100644 --- a/squad-server/utils/any-id.js +++ b/squad-server/utils/any-id.js @@ -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; } /**