Explorar el Código

Add cooldown for non-users

poeti8 hace 6 años
padre
commit
0a68a7437e

+ 4 - 0
.example.env

@@ -18,6 +18,10 @@ REDIS_PASSWORD=
 # The daily limit for each user
 USER_LIMIT_PER_DAY=50
 
+# Create a cooldown for non-users in minutes
+# Set 0 to disable
+NON_USER_COOLDOWN=0
+
 # A passphrase to encrypt JWT. Use a long and secure key.
 JWT_SECRET=securekey
 

+ 19 - 0
package-lock.json

@@ -7811,6 +7811,15 @@
         "semver": "^5.5.0"
       }
     },
+    "node-cron": {
+      "version": "2.0.3",
+      "resolved": "https://registry.npmjs.org/node-cron/-/node-cron-2.0.3.tgz",
+      "integrity": "sha512-eJI+QitXlwcgiZwNNSRbqsjeZMp5shyajMR81RZCqeW0ZDEj4zU9tpd4nTh/1JsBiKbF8d08FCewiipDmVIYjg==",
+      "requires": {
+        "opencollective-postinstall": "^2.0.0",
+        "tz-offset": "0.0.1"
+      }
+    },
     "node-environment-flags": {
       "version": "1.0.5",
       "resolved": "https://registry.npmjs.org/node-environment-flags/-/node-environment-flags-1.0.5.tgz",
@@ -8076,6 +8085,11 @@
         "mimic-fn": "^1.0.0"
       }
     },
+    "opencollective-postinstall": {
+      "version": "2.0.2",
+      "resolved": "https://registry.npmjs.org/opencollective-postinstall/-/opencollective-postinstall-2.0.2.tgz",
+      "integrity": "sha512-pVOEP16TrAO2/fjej1IdOyupJY8KDUM1CvsaScRbw6oddvpQoOfGk4ywha0HKKVAD6RkW4x6Q+tNBwhf3Bgpuw=="
+    },
     "optionator": {
       "version": "0.8.2",
       "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.8.2.tgz",
@@ -10215,6 +10229,11 @@
       "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz",
       "integrity": "sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c="
     },
+    "tz-offset": {
+      "version": "0.0.1",
+      "resolved": "https://registry.npmjs.org/tz-offset/-/tz-offset-0.0.1.tgz",
+      "integrity": "sha512-kMBmblijHJXyOpKzgDhKx9INYU4u4E1RPMB0HqmKSgWG8vEcf3exEfLh4FFfzd3xdQOw9EuIy/cP0akY6rHopQ=="
+    },
     "uglify-es": {
       "version": "3.3.9",
       "resolved": "https://registry.npmjs.org/uglify-es/-/uglify-es-3.3.9.tgz",

+ 1 - 0
package.json

@@ -54,6 +54,7 @@
     "neo4j-driver": "^1.7.2",
     "next": "^7.0.3",
     "next-redux-wrapper": "^2.1.0",
+    "node-cron": "^2.0.3",
     "nodemailer": "^4.7.0",
     "passport": "^0.4.0",
     "passport-jwt": "^4.0.0",

+ 5 - 1
server/controllers/urlController.js

@@ -8,6 +8,7 @@ const geoip = require('geoip-lite');
 const bcrypt = require('bcryptjs');
 const ua = require('universal-analytics');
 const isbot = require('isbot');
+const { addIPCooldown } = require('../db/user');
 const {
   createShortUrl,
   createVisit,
@@ -42,7 +43,7 @@ const generateId = async () => {
   return generateId();
 };
 
-exports.urlShortener = async ({ body, user }, res) => {
+exports.urlShortener = async ({ body, realIp, user }, res) => {
   try {
     const domain = URL.parse(body.target).hostname;
 
@@ -90,6 +91,9 @@ exports.urlShortener = async ({ body, user }, res) => {
     const id = (user && body.customurl) || queries[5];
     const target = addProtocol(body.target);
     const url = await createShortUrl({ ...body, id, target, user });
+    if (!user && Number(process.env.NON_USER_COOLDOWN)) {
+      addIPCooldown(realIp);
+    }
 
     return res.json(url);
   } catch (error) {

+ 15 - 2
server/controllers/validateBodyController.js

@@ -4,9 +4,9 @@ const axios = require('axios');
 const URL = require('url');
 const urlRegex = require('url-regex');
 const validator = require('express-validator/check');
-const { subHours } = require('date-fns/');
+const { differenceInMinutes, subHours } = require('date-fns/');
 const { validationResult } = require('express-validator/check');
-const { addCooldown, banUser } = require('../db/user');
+const { addCooldown, banUser, getIPCooldown: getIPCooldownCount } = require('../db/user');
 const { getBannedDomain, getBannedHost, urlCountFromDate } = require('../db/url');
 const subDay = require('date-fns/sub_days');
 const { addProtocol } = require('../utils');
@@ -123,6 +123,19 @@ exports.cooldownCheck = async user => {
   }
 };
 
+exports.ipCooldownCheck = async (req, res, next) => {
+  const cooldonwConfig = Number(process.env.NON_USER_COOLDOWN);
+  if (req.user || !cooldonwConfig) return next();
+  const cooldownDate = await getIPCooldownCount(req.realIp);
+  if (cooldownDate) {
+    const timeToWait = cooldonwConfig - differenceInMinutes(new Date(), cooldownDate);
+    return res
+      .status(400)
+      .json({ error: `Non-users are limited. Wait ${timeToWait} minutes or log in.` });
+  }
+  next();
+};
+
 exports.malwareCheck = async (user, target) => {
   const isMalware = await axios.post(
     `https://safebrowsing.googleapis.com/v4/threatMatches:find?key=${

+ 8 - 0
server/cron.js

@@ -0,0 +1,8 @@
+const cron = require('node-cron');
+const { clearIPs } = require('./db/user');
+
+if (Number(process.env.NON_USER_COOLDOWN)) {
+  cron.schedule('* */24 * * *', () => {
+    clearIPs().catch();
+  });
+}

+ 48 - 0
server/db/user.js

@@ -1,5 +1,6 @@
 const bcrypt = require('bcryptjs');
 const nanoid = require('nanoid');
+const subMinutes = require('date-fns/sub_minutes');
 const driver = require('./neo4j');
 
 exports.getUser = ({ email = '', apikey = '' }) =>
@@ -228,3 +229,50 @@ exports.banUser = ({ email }) =>
       })
       .catch(err => reject(err));
   });
+
+exports.addIPCooldown = async ip => {
+  const session = driver.session();
+  const { records = [] } = await session.writeTransaction(tx =>
+    tx.run(
+      'MERGE (i:IP { ip: $ip }) ' +
+        'MERGE (i)-[r:RECEIVED]->(c:COOLDOWN { date: $date }) ' +
+        'RETURN COUNT(r) as count',
+      {
+        date: new Date().toJSON(),
+        ip,
+      }
+    )
+  );
+  session.close();
+  const count = records.length && records[0].get('count').toNumber();
+  return count;
+};
+
+exports.getIPCooldown = async ip => {
+  const session = driver.session();
+  const { records = [] } = await session.readTransaction(tx =>
+    tx.run(
+      'MATCH (i:IP { ip: $ip }) ' +
+        'MATCH (i)-[:RECEIVED]->(c:COOLDOWN) ' +
+        'WHERE c.date > $date ' +
+        'RETURN c.date as date',
+      {
+        date: subMinutes(new Date(), Number(process.env.NON_USER_COOLDOWN)).toJSON(),
+        ip,
+      }
+    )
+  );
+  session.close();
+  const count = records.length && records[0].get('date');
+  return count;
+};
+
+exports.clearIPs = async () => {
+  const session = driver.session();
+  await session.writeTransaction(tx =>
+    tx.run('MATCH (i:IP)-[:RECEIVED]->(c:COOLDOWN) WHERE c.date < $date DETACH DELETE i, c', {
+      date: subMinutes(new Date(), Number(process.env.NON_USER_COOLDOWN)).toJSON(),
+    })
+  );
+  session.close();
+};

+ 3 - 0
server/server.js

@@ -13,10 +13,12 @@ const {
   validateBody,
   validationCriterias,
   validateUrl,
+  ipCooldownCheck,
 } = require('./controllers/validateBodyController');
 const auth = require('./controllers/authController');
 const url = require('./controllers/urlController');
 
+require('./cron');
 require('./passport');
 
 if (process.env.RAVEN_DSN) {
@@ -100,6 +102,7 @@ app.prepare().then(() => {
     auth.authJwtLoose,
     catchErrors(auth.recaptcha),
     catchErrors(validateUrl),
+    catchErrors(ipCooldownCheck),
     catchErrors(url.urlShortener)
   );
   server.post('/api/url/deleteurl', auth.authApikey, auth.authJwt, catchErrors(url.deleteUrl));