1
0
Fork 0
mirror of https://github.com/plankanban/planka.git synced 2025-07-18 20:59:44 +02:00

feat: Modify logger to log to file that supports fail2ban (#284)

This commit is contained in:
Steven Correia 2022-08-22 18:42:56 -04:00 committed by GitHub
parent 5a27ac0f03
commit fe5fe5fab7
12 changed files with 427 additions and 7844 deletions

View file

@ -1,6 +1,8 @@
const bcrypt = require('bcrypt');
const validator = require('validator');
const { getRemoteAddress } = require('../../../utils/remoteAddress');
const Errors = {
INVALID_EMAIL_OR_USERNAME: {
invalidEmailOrUsername: 'Invalid email or username',
@ -41,10 +43,16 @@ module.exports = {
const user = await sails.helpers.users.getOneByEmailOrUsername(inputs.emailOrUsername);
if (!user) {
sails.log.warn(
`Invalid email or username: "${inputs.emailOrUsername}"! (IP: ${getRemoteAddress(
this.req,
)})`,
);
throw Errors.INVALID_EMAIL_OR_USERNAME;
}
if (!bcrypt.compareSync(inputs.password, user.password)) {
sails.log.warn(`Invalid password! (IP: ${getRemoteAddress(this.req)})`);
throw Errors.INVALID_PASSWORD;
}

View file

@ -243,9 +243,7 @@ module.exports = {
*
*/
log: {
level: 'debug',
},
log: {},
http: {
/**

View file

@ -64,7 +64,5 @@ module.exports = {
},
},
log: {
level: 'warn',
},
log: {},
};

View file

@ -10,6 +10,8 @@
* https://sailsjs.com/docs/concepts/logging
*/
const { customLogger } = require('../utils/logger');
module.exports.log = {
/**
*
@ -22,5 +24,20 @@ module.exports.log = {
* You may also set the level to "silent" to suppress all logs.
*
*/
// level: 'info',
/**
* Passthrough plain log message(s) to
* custom Winston console and file logger.
*
* Note that Winston's log levels override Sails' log levels.
* Refer: https://github.com/winstonjs/winston#logging
*/
custom: customLogger,
inspect: false,
/**
* Removes the Sail.js init success logs
* (ASCII ship art) for production instances.
*/
noShip: process.env.NODE_ENV === 'production',
};

8002
server/package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -10,7 +10,7 @@
"lint": "eslint . --max-warnings=0 --report-unused-disable-directives && echo '✔ Your .js files look good.'",
"start": "nodemon",
"start:prod": "node app.js --prod",
"test": "mocha test/lifecycle.test.js test/integration/**/*.test.js"
"test": "mocha test/lifecycle.test.js test/integration/**/*.test.js test/utils/**/*.test.js"
},
"eslintConfig": {
"plugins": [
@ -54,7 +54,8 @@
"sharp": "^0.30.7",
"stream-to-array": "^2.3.0",
"uuid": "^8.3.2",
"validator": "^13.7.0"
"validator": "^13.7.0",
"winston": "^3.8.1"
},
"devDependencies": {
"chai": "^4.3.6",

View file

@ -0,0 +1,78 @@
const { expect } = require('chai');
const { getRemoteAddress } = require('../../utils/remoteAddress');
/**
* Fake HTTP request object
* given to all api controllers.
*/
const MOCK_REQUEST = {
ip: '',
ips: [],
};
/**
* Mocks the `MOCK_REQUEST` value.
* Should be called before asserting `getRemoteAddress`.
* @param {string} ip Mock remote IP address
* @param {any[]} ips Mock array of proxy IP addresses
*/
const mockRequest = (ip, ips) => {
MOCK_REQUEST.ip = ip;
MOCK_REQUEST.ips = ips;
};
/**
* Mocks the `TRUST_PROXY` environment variable passed through `docker-compose` file.
* @param {boolean} trustProxy Whether the TRUST_PROXY environment variable was enabled.
*/
const mockProxyFlag = (trustProxy) => {
process.env.TRUST_PROXY = !!trustProxy;
};
describe('remoteAddress', () => {
describe('#getRemoteAddress(Request)', () => {
it('should get IPv4 remote address while not behind proxy and TRUST_PROXY=false', async () => {
const expectedAddress = '172.2.109.132';
mockRequest(`::ffff:${expectedAddress}`, null);
mockProxyFlag(false);
expect(getRemoteAddress(MOCK_REQUEST)).to.be.equal(expectedAddress);
});
it('should get IPv6 remote address while not behind proxy and TRUST_PROXY=false', async () => {
const expectedAddress = 'f53f:5832:9f1c:fe38:ce3d:1be8:81a2:115e';
mockRequest(expectedAddress, null);
mockProxyFlag(false);
expect(getRemoteAddress(MOCK_REQUEST)).to.be.equal(expectedAddress);
});
it('should get IPv4 remote address while behind proxy and TRUST_PROXY=true', async () => {
const expectedAddress = '172.2.109.132';
mockRequest(`::ffff:${expectedAddress}`, [
`::ffff:${expectedAddress}`,
'::ffff:192.182.23.111',
'::ffff:120.210.132.14',
]);
mockProxyFlag(true);
expect(getRemoteAddress(MOCK_REQUEST)).to.be.equal(expectedAddress);
});
it('should get IPv6 remote address while behind proxy and TRUST_PROXY=true', async () => {
const expectedAddress = 'f53f:5832:9f1c:fe38:ce3d:1be8:81a2:115e';
mockRequest(expectedAddress, [
expectedAddress,
'9d74:fb18:3b95:801f:8751:8d18:8207:b322',
'598e:4291:e1b3:2991:5d17:00af:1b6b:802c',
]);
mockProxyFlag(true);
expect(getRemoteAddress(MOCK_REQUEST)).to.be.equal(expectedAddress);
});
});
});

42
server/utils/logger.js Normal file
View file

@ -0,0 +1,42 @@
const winston = require('winston');
/**
* The default timestamp used by the logger.
* Format example: "2022-08-18 6:30:02"
*/
const defaultLogTimestampFormat = 'YYYY-MM-DD HH:mm:ss';
const logfile = `${process.cwd()}/logs/planka.log`;
/**
* Log level for both console and file log sinks.
*
* Refer {@link https://github.com/winstonjs/winston#logging here}
* for more information on Winston log levels.
*/
const logLevel = process.env.NODE_ENV === 'production' ? 'info' : 'debug';
const logFormat = winston.format.combine(
winston.format.uncolorize(),
winston.format.timestamp({ format: defaultLogTimestampFormat }),
winston.format.printf((log) => `${log.timestamp} [${log.level[0].toUpperCase()}] ${log.message}`),
);
// eslint-disable-next-line new-cap
const customLogger = new winston.createLogger({
transports: [
new winston.transports.File({
level: logLevel,
format: logFormat,
filename: logfile,
}),
new winston.transports.Console({
level: logLevel,
format: logFormat,
}),
],
});
module.exports = {
customLogger,
};

View file

@ -0,0 +1,27 @@
/**
* The IP address of the client that just made a request to this application, whether
* or not the TRUST_PROXY env variable is true and if endpoint accessed through a proxy.
* @param {Request} request The endpoint Request object
* @returns The IP address of the client that just made a request
*/
const getRemoteAddress = (request) => {
let remoteAddress = request.ip;
// Assert if "X-Forwarded-For" header contains any addresses
if (process.env.TRUST_PROXY && !_.isEmpty(request.ips)) {
// eslint-disable-next-line prefer-destructuring
remoteAddress = request.ips[0];
}
// Convert address from IPV6 to IPV4 if client device is IPV4.
const defaultIPV6Regex = /^::ffff:((25[0-5]|(2[0-4]|1\d|[1-9]|)\d)\.?\b){4}$/g;
if (remoteAddress.match(defaultIPV6Regex)) {
remoteAddress = remoteAddress.replace('::ffff:', '');
}
return remoteAddress;
};
module.exports = {
getRemoteAddress,
};