Переглянути джерело

Add API limits configurable in settings.json

Gaël Métais 10 роки тому
батько
коміт
0da2ad0c10

+ 4 - 1
bin/server.js

@@ -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

+ 0 - 52
lib/server/authMiddleware.js

@@ -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;

+ 85 - 0
lib/server/middlewares/apiLimitsMiddleware.js

@@ -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 - 0
lib/server/middlewares/authMiddleware.js

@@ -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;

+ 0 - 1
package.json

@@ -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
server_config/settings.json

@@ -5,8 +5,6 @@
     "authorizedKeys": {
         "1234567890": "contact@gaelmetais.com"
     },
-    "tokenSalt": "lake-city",
-    "authorizedApplications": [
-        "frontend"
-    ]
+    "maxAnonymousRunsPerDay": 24,
+    "maxAnonymousCallsPerDay": 1000
 }

+ 72 - 97
test/api/apiTest.js

@@ -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);
-
-        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) {
+    it('should accept up to 24 anonymous runs to the API', function(done) {
+        this.timeout(15000);
+
+        function launchRun() {
+            var deferred = Q.defer();
+
+            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);
+                }
+            });
+
+            return deferred.promise;
+        }
+
+        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) {
+            
+            // Here should still be ok
+            response.statusCode.should.equal(200);
+
+            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);
-            }
-        });
-    });
 
-    it('should refuse a token from an unknown app', function(done) {
-        this.timeout(5000);
+            })
+            .fail(function(error) {
+                done(error);
+            });
 
-        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);
-            }
+        }).fail(function(error) {
+            done(error);
         });
+        
     });
 
-    it('should accept a good token', function(done) {
-        this.timeout(5000);
 
-        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) {
-
-                runId = body.runId;
-                runId.should.be.a('string');
-                done();
-
-            } else {
-                done(error || response.statusCode);
-            }
-        });
-    });
 });