diff --git a/src/api/ApiStatusCodes.ts b/src/api/ApiStatusCodes.ts index ceb5309..7dcbb9a 100644 --- a/src/api/ApiStatusCodes.ts +++ b/src/api/ApiStatusCodes.ts @@ -45,6 +45,7 @@ class ApiStatusCodes { static readonly ILLEGAL_PARAMETER = 1110 static readonly NOT_FOUND = 1111 static readonly AUTHENTICATION_FAILED = 1112 + static readonly STATUS_PASSWORD_BACK_OFF = 1113 } export = ApiStatusCodes diff --git a/src/routes/login/LoginRouter.ts b/src/routes/login/LoginRouter.ts index c976252..00ea0d0 100644 --- a/src/routes/login/LoginRouter.ts +++ b/src/routes/login/LoginRouter.ts @@ -6,9 +6,12 @@ import InjectionExtractor = require('../../injection/InjectionExtractor') import DataStoreProvider = require('../../datastore/DataStoreProvider') import CaptainManager = require('../../user/system/CaptainManager') import Authenticator = require('../../user/Authenticator') +import CircularQueue from '../../utils/CircularQueue' const router = express.Router() +const failedLoginCircularTimestamps = new CircularQueue(5) + router.post('/', function(req, res, next) { let password = req.body.password || '' @@ -30,6 +33,16 @@ router.post('/', function(req, res, next) { Promise.resolve() // .then(function() { + const oldestKnownFailedLogin = failedLoginCircularTimestamps.peek() + if ( + oldestKnownFailedLogin && + new Date().getTime() - oldestKnownFailedLogin < 30000 + ) + throw ApiStatusCodes.createError( + ApiStatusCodes.STATUS_PASSWORD_BACK_OFF, + 'Too many wrong passwords... Wait for 30 seconds and retry.' + ) + return DataStoreProvider.getDataStore(namespace).getHashedPassword() }) .then(function(savedHashedPassword) { @@ -54,6 +67,19 @@ router.post('/', function(req, res, next) { baseApi.data = { token: authToken } res.send(baseApi) }) + .catch(function(err) { + return new Promise(function(resolve, reject) { + if ( + err && + err.captainErrorType && + err.captainErrorType === + ApiStatusCodes.STATUS_WRONG_PASSWORD + ) { + failedLoginCircularTimestamps.push(new Date().getTime()) + } + reject(err) + }) + }) .catch(ApiStatusCodes.createCatcher(res)) }) diff --git a/src/utils/CircularQueue.ts b/src/utils/CircularQueue.ts new file mode 100644 index 0000000..1c16327 --- /dev/null +++ b/src/utils/CircularQueue.ts @@ -0,0 +1,23 @@ + +export default class CircularQueue { + private values: (T | undefined)[] = [] + private currSize = 0 + + constructor(private maxSize: number) { + if (!this.maxSize) throw new Error('invalid size of zero') + if (this.maxSize === 1) throw new Error('invalid size of one') + for (let index = 0; index < this.maxSize; index++) { + this.values.push(undefined) + } + } + + push(value: T) { + this.values[this.currSize % this.maxSize] = value + this.currSize++ + } + + peek(): T | undefined { + const nextPositionToBeOverwritten = this.currSize % this.maxSize + return this.values[nextPositionToBeOverwritten] + } +} diff --git a/tests/CircularQueue.test.ts b/tests/CircularQueue.test.ts new file mode 100644 index 0000000..968d840 --- /dev/null +++ b/tests/CircularQueue.test.ts @@ -0,0 +1,38 @@ +import CircularQueue from '../src/utils/CircularQueue' + +function createTest(size: number, initWith: number) { + const testQueue = new CircularQueue(size) + for (let index = 0; index < initWith; index++) { + testQueue.push('val:' + (index + 1)) + } + + return testQueue +} + +test('Large Circular Queue', () => { + for (let index = 5; index < 20; index++) { + expect(createTest(4, index).peek()) // + .toBe('val:' + (index - 3)) + } +}) + +test('Basic Circular Queue', () => { + expect(createTest(2, 10).peek()) // + .toBe('val:9') + +}) + +test('Basic Circular Queue', () => { + expect(createTest(2, 1).peek()) // + .toBe(undefined) +}) + +test('Basic Circular Queue', () => { + expect(createTest(2, 2).peek()) // + .toBe('val:1') +}) + +test('Basic Circular Queue', () => { + expect(createTest(2, 3).peek()) // + .toBe('val:2') +})