Add API limits configurable in settings.json

This commit is contained in:
Gaël Métais
2014-12-12 20:26:19 +01:00
parent 84faabca86
commit 0da2ad0c10
7 changed files with 202 additions and 152 deletions
+4 -1
View File
@@ -7,11 +7,14 @@ var server = require('http').createServer(app);
var bodyParser = require('body-parser');
var compress = require('compression');
var authMiddleware = require('../lib/server/authMiddleware');
var authMiddleware = require('../lib/server/middlewares/authMiddleware');
var apiLimitsMiddleware = require('../lib/server/middlewares/apiLimitsMiddleware');
app.use(compress());
app.use(bodyParser.json());
app.use(authMiddleware);
app.use(apiLimitsMiddleware);
// Initialize the controllers
-52
View File
@@ -1,52 +0,0 @@
var config = require('../../server_config/settings.json');
var jwt = require('jwt-simple');
var debug = require('debug')('authMiddleware');
var authMiddleware = function(req, res, next) {
'use strict';
if (req.path.indexOf('/api/') === 0) {
// Test if it's an authorized key
if (req.headers && req.headers['x-api-key'] && isApiKeyValid(req.headers['x-api-key'])) {
next();
return;
}
// Test if it's an authorized token
if (req.headers && req.headers['x-api-token'] && isTokenValid(req.headers['x-api-token'])) {
next();
return;
}
res.status(401).send('Unauthorized');
}
};
function isApiKeyValid(apiKey) {
return (config.authorizedKeys[apiKey]) ? true : false;
}
function isTokenValid(token) {
var data = null;
try {
data = jwt.decode(token, config.tokenSalt);
} catch(err) {
debug('Error while decoding token');
debug(err);
return false;
}
return data.expire &&
data.expire > Date.now() &&
data.application &&
config.authorizedApplications.indexOf(data.application) >= 0;
}
module.exports = authMiddleware;
@@ -0,0 +1,85 @@
var config = require('../../../server_config/settings.json');
var debug = require('debug')('apiLimitsMiddleware');
var apiLimitsMiddleware = function(req, res, next) {
'use strict';
debug('Entering API Limits Middleware with IP address %s', req.connection.remoteAddress);
if (req.path.indexOf('/api/') === 0 && !res.locals.hasApiKey) {
if (req.path === '/api/runs') {
if (!runsTable.accepts(req.connection.remoteAddress)) {
// Sorry :/
debug('Too many tests launched from IP address %s', req.connection.remoteAddress);
res.status(429).send('Too Many Requests');
return;
}
}
if (!callsTable.accepts(req.connection.remoteAddress)) {
// Sorry :/
debug('Too many API requests from IP address %s', req.connection.remoteAddress);
res.status(429).send('Too Many Requests');
return;
}
debug('Not blocked by the API limits');
// It's ok for the moment
}
next();
};
var RecordTable = function(maxPerDay) {
var table = {};
// Check if the user overpassed the limit and save its visit
this.accepts = function(ipAddress) {
if (table[ipAddress]) {
this.cleanEntry(ipAddress);
debug('%d visits in the last 24 hours', table[ipAddress].length);
if (table[ipAddress].length >= maxPerDay) {
return false;
} else {
table[ipAddress].push(Date.now());
}
} else {
table[ipAddress] = [];
table[ipAddress].push(Date.now());
}
return true;
};
// Clean the table for this guy
this.cleanEntry = function(ipAddress) {
table[ipAddress] = table[ipAddress].filter(function(date) {
return date > Date.now() - 1000*60*60*24;
});
};
// Clean the entire table once in a while
this.removeOld = function() {
for (var ipAddress in table) {
this.cleanEntry(ipAddress);
}
};
};
// Init the records tables
var runsTable = new RecordTable(config.maxAnonymousRunsPerDay);
var callsTable = new RecordTable(config.maxAnonymousCallsPerDay);
module.exports = apiLimitsMiddleware;
+42
View File
@@ -0,0 +1,42 @@
var config = require('../../../server_config/settings.json');
var debug = require('debug')('authMiddleware');
var authMiddleware = function(req, res, next) {
'use strict';
if (req.path.indexOf('/api/') === 0) {
if (req.headers && req.headers['x-api-key']) {
// Test if it's an authorized key
if (isApiKeyValid(req.headers['x-api-key'])) {
// Come in!
debug('Authorized key: %s', req.headers['x-api-key']);
res.locals.hasApiKey = true;
} else {
// Sorry :/
debug('Unauthorized key %s', req.headers['x-api-key']);
res.status(401).send('Unauthorized');
return;
}
} else {
debug('No authorization key');
// It's ok for the moment but you might be blocked by the apiLimitsMiddleware, dude
}
}
next();
};
function isApiKeyValid(apiKey) {
return (config.authorizedKeys[apiKey]) ? true : false;
}
module.exports = authMiddleware;
-1
View File
@@ -15,7 +15,6 @@
"compression": "~1.2.1",
"debug": "^2.1.0",
"express": "~4.10.4",
"jwt-simple": "^0.2.0",
"phantomas": "1.7.0",
"rimraf": "^2.2.8",
"socket.io": "~1.2.0"
+2 -4
View File
@@ -5,8 +5,6 @@
"authorizedKeys": {
"1234567890": "contact@gaelmetais.com"
},
"tokenSalt": "lake-city",
"authorizedApplications": [
"frontend"
]
"maxAnonymousRunsPerDay": 24,
"maxAnonymousCallsPerDay": 1000
}
+69 -94
View File
@@ -1,13 +1,11 @@
var should = require('chai').should();
var request = require('request');
var jwt = require('jwt-simple');
var Q = require('q');
var config = {
"authorizedKeys": {
"1234567890": "contact@gaelmetais.com"
},
"tokenSalt": "lake-city",
"authorizedApplications": ["frontend"]
}
};
var apiUrl = 'http://localhost:8387/api';
@@ -16,26 +14,7 @@ var wwwUrl = 'http://localhost:8388';
describe('api', function() {
var runId;
it('should not accept a query if there is no key in headers', function(done) {
this.timeout(5000);
request({
method: 'POST',
url: apiUrl + '/runs',
body: {
url: wwwUrl + '/simple-page.html',
waitForResponse: false
},
json: true
}, function(error, response, body) {
if (!error && response.statusCode === 401) {
done();
} else {
done(error || response.statusCode);
}
});
});
it('should refuse a query with an invalid key', function(done) {
this.timeout(5000);
@@ -87,85 +66,81 @@ describe('api', function() {
});
});
it('should refuse an expired token', function(done) {
this.timeout(5000);
it('should accept up to 24 anonymous runs to the API', function(done) {
this.timeout(15000);
request({
method: 'POST',
url: apiUrl + '/runs',
body: {
url: wwwUrl + '/simple-page.html',
waitForResponse: false
},
json: true,
headers: {
'X-Api-Token': jwt.encode({
application: config.authorizedApplications[0],
expire: Date.now() - 60000
}, config.tokenSalt)
}
}, function(error, response, body) {
if (!error && response.statusCode === 401) {
done();
} else {
done(error || response.statusCode);
}
});
});
function launchRun() {
var deferred = Q.defer();
it('should refuse a token from an unknown app', function(done) {
this.timeout(5000);
request({
method: 'POST',
url: apiUrl + '/runs',
body: {
url: wwwUrl + '/simple-page.html',
waitForResponse: false
},
json: true
}, function(error, response, body) {
if (error) {
deferred.reject(error);
} else {
deferred.resolve(response, body);
}
});
request({
method: 'POST',
url: apiUrl + '/runs',
body: {
url: wwwUrl + '/simple-page.html',
waitForResponse: false
},
json: true,
headers: {
'X-Api-Token': jwt.encode({
application: 'unknown-app',
expire: Date.now() + 60000
}, config.tokenSalt)
}
}, function(error, response, body) {
if (!error && response.statusCode === 401) {
done();
} else {
done(error || response.statusCode);
}
});
});
return deferred.promise;
}
it('should accept a good token', function(done) {
this.timeout(5000);
launchRun()
.then(launchRun)
.then(launchRun)
.then(launchRun)
.then(launchRun)
.then(launchRun)
.then(launchRun)
.then(launchRun)
.then(launchRun)
.then(launchRun)
.then(launchRun)
.then(launchRun)
request({
method: 'POST',
url: apiUrl + '/runs',
body: {
url: wwwUrl + '/simple-page.html',
waitForResponse: false
},
json: true,
headers: {
'X-Api-Token': jwt.encode({
application: config.authorizedApplications[0],
expire: Date.now() + 60000
}, config.tokenSalt)
}
}, function(error, response, body) {
if (!error && response.statusCode === 200) {
.then(function(response, body) {
// Here should still be ok
response.statusCode.should.equal(200);
runId = body.runId;
runId.should.be.a('string');
launchRun()
.then(launchRun)
.then(launchRun)
.then(launchRun)
.then(launchRun)
.then(launchRun)
.then(launchRun)
.then(launchRun)
.then(launchRun)
.then(launchRun)
.then(launchRun)
.then(launchRun)
.then(launchRun)
.then(launchRun)
.then(launchRun)
.then(function(response, body) {
// It should fail now
response.statusCode.should.equal(429);
done();
} else {
done(error || response.statusCode);
}
})
.fail(function(error) {
done(error);
});
}).fail(function(error) {
done(error);
});
});
});