Add API limits configurable in settings.json
This commit is contained in:
+4
-1
@@ -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
|
||||
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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"
|
||||
|
||||
@@ -5,8 +5,6 @@
|
||||
"authorizedKeys": {
|
||||
"1234567890": "contact@gaelmetais.com"
|
||||
},
|
||||
"tokenSalt": "lake-city",
|
||||
"authorizedApplications": [
|
||||
"frontend"
|
||||
]
|
||||
"maxAnonymousRunsPerDay": 24,
|
||||
"maxAnonymousCallsPerDay": 1000
|
||||
}
|
||||
+69
-94
@@ -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);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user