Bläddra i källkod

Run async shorrtening task in parallel to speed up the process

poeti8 7 år sedan
förälder
incheckning
fdf98f8c5f
4 ändrade filer med 109 tillägg och 85 borttagningar
  1. 54 63
      server/controllers/urlController.js
  2. 50 15
      server/controllers/validateBodyController.js
  3. 5 3
      server/db/user.js
  4. 0 4
      server/server.js

+ 54 - 63
server/controllers/urlController.js

@@ -1,12 +1,11 @@
+const { promisify } = require('util');
 const urlRegex = require('url-regex');
-const URL = require('url');
 const dns = require('dns');
-const { promisify } = require('util');
+const URL = require('url');
 const generate = require('nanoid/generate');
 const useragent = require('useragent');
 const geoip = require('geoip-lite');
 const bcrypt = require('bcryptjs');
-const subDay = require('date-fns/sub_days');
 const ua = require('universal-analytics');
 const isbot = require('isbot');
 const {
@@ -20,12 +19,16 @@ const {
   getStats,
   getUrls,
   setCustomDomain,
-  urlCountFromDate,
   banUrl,
-  getBannedDomain,
-  getBannedHost,
 } = require('../db/url');
-const { preservedUrls } = require('./validateBodyController');
+const {
+  checkBannedDomain,
+  checkBannedHost,
+  cooldownCheck,
+  malwareCheck,
+  preservedUrls,
+  urlCountsCheck,
+} = require('./validateBodyController');
 const transporter = require('../mail/mail');
 const redis = require('../redis');
 const { addProtocol, generateShortUrl, getStatsCacheTime } = require('../utils');
@@ -41,70 +44,58 @@ const generateId = async () => {
 };
 
 exports.urlShortener = async ({ body, user }, res) => {
-  // Check if user has passed daily limit
-  if (user) {
-    const { count } = await urlCountFromDate({
-      email: user.email,
-      date: subDay(new Date(), 1).toJSON(),
-    });
-    if (count > config.USER_LIMIT_PER_DAY) {
-      return res.status(429).json({
-        error: `You have reached your daily limit (${config.USER_LIMIT_PER_DAY}). Please wait 24h.`,
-      });
-    }
-  }
-
-  // if "reuse" is true, try to return
-  // the existent URL without creating one
-  if (user && body.reuse) {
-    const urls = await findUrl({ target: addProtocol(body.target) });
-    if (urls.length) {
-      urls.sort((a, b) => a.createdAt > b.createdAt);
-      const { domain: d, user: u, ...url } = urls[urls.length - 1];
-      const data = {
-        ...url,
-        password: !!url.password,
-        reuse: true,
-        shortUrl: generateShortUrl(url.id, user.domain, user.useHttps),
-      };
-      return res.json(data);
+  try {
+    const domain = URL.parse(body.target).hostname;
+
+    const queries = await Promise.all([
+      config.GOOGLE_SAFE_BROWSING_KEY && cooldownCheck(user),
+      config.GOOGLE_SAFE_BROWSING_KEY && malwareCheck(user, body.target),
+      user && urlCountsCheck(user.email),
+      user && body.reuse && findUrl({ target: addProtocol(body.target) }),
+      user && body.customurl && findUrl({ id: body.customurl || '' }),
+      (!user || !body.customurl) && generateId(),
+      checkBannedDomain(domain),
+      checkBannedHost(domain),
+    ]);
+
+    // if "reuse" is true, try to return
+    // the existent URL without creating one
+    if (user && body.reuse) {
+      const urls = queries[3];
+      if (urls.length) {
+        urls.sort((a, b) => a.createdAt > b.createdAt);
+        const { domain: d, user: u, ...url } = urls[urls.length - 1];
+        const data = {
+          ...url,
+          password: !!url.password,
+          reuse: true,
+          shortUrl: generateShortUrl(url.id, user.domain, user.useHttps),
+        };
+        return res.json(data);
+      }
     }
-  }
 
-  // Check if custom URL already exists
-  if (user && body.customurl) {
-    const urls = await findUrl({ id: body.customurl || '' });
-    if (urls.length) {
-      const urlWithNoDomain = !user.domain && urls.some(url => !url.domain);
-      const urlWithDmoain = user.domain && urls.some(url => url.domain === user.domain);
-      if (urlWithNoDomain || urlWithDmoain) {
-        return res.status(400).json({ error: 'Custom URL is already in use.' });
+    // Check if custom URL already exists
+    if (user && body.customurl) {
+      const urls = queries[4];
+      if (urls.length) {
+        const urlWithNoDomain = !user.domain && urls.some(url => !url.domain);
+        const urlWithDmoain = user.domain && urls.some(url => url.domain === user.domain);
+        if (urlWithNoDomain || urlWithDmoain) {
+          throw new Error('Custom URL is already in use.');
+        }
       }
     }
-  }
 
-  // If domain or host is banned
-  const domain = URL.parse(body.target).hostname;
-  const isDomainBanned = await getBannedDomain(domain);
+    // Create new URL
+    const id = (user && body.customurl) || queries[5];
+    const target = addProtocol(body.target);
+    const url = await createShortUrl({ ...body, id, target, user });
 
-  let isHostBanned;
-  try {
-    const dnsRes = await dnsLookup(domain);
-    isHostBanned = await getBannedHost(dnsRes && dnsRes.address);
+    return res.json(url);
   } catch (error) {
-    isHostBanned = null;
+    return res.status(400).json({ error: error.message });
   }
-
-  if (isDomainBanned || isHostBanned) {
-    return res.status(400).json({ error: 'URL is containing malware/scam.' });
-  }
-
-  // Create new URL
-  const id = (user && body.customurl) || (await generateId());
-  const target = addProtocol(body.target);
-  const url = await createShortUrl({ ...body, id, target, user });
-
-  return res.json(url);
 };
 
 const browsersList = ['IE', 'Firefox', 'Chrome', 'Opera', 'Safari', 'Edge'];

+ 50 - 15
server/controllers/validateBodyController.js

@@ -1,10 +1,16 @@
+const { promisify } = require('util');
+const dns = require('dns');
 const axios = require('axios');
 const urlRegex = require('url-regex');
 const validator = require('express-validator/check');
 const { subHours } = require('date-fns/');
 const { validationResult } = require('express-validator/check');
-const { addCooldown, banUser, getCooldowns } = require('../db/user');
+const { addCooldown, banUser } = require('../db/user');
+const { getBannedDomain, getBannedHost, urlCountFromDate } = require('../db/url');
 const config = require('../config');
+const subDay = require('date-fns/sub_days');
+
+const dnsLookup = promisify(dns.lookup);
 
 exports.validationCriterias = [
   validator
@@ -95,22 +101,22 @@ exports.validateUrl = async ({ body, user }, res, next) => {
   return next();
 };
 
-exports.cooldownCheck = async ({ user }, res, next) => {
-  if (user) {
-    const { cooldowns } = await getCooldowns(user);
-    if (cooldowns.length > 4) {
+exports.cooldownCheck = async user => {
+  if (user && user.cooldowns) {
+    if (user.cooldowns.length > 4) {
       await banUser(user);
-      return res.status(400).json({ error: 'Too much malware requests. You are now banned.' });
+      throw new Error('Too much malware requests. You are now banned.');
     }
-    const hasCooldownNow = cooldowns.some(cooldown => cooldown > subHours(new Date(), 12).toJSON());
+    const hasCooldownNow = user.cooldowns.some(
+      cooldown => cooldown > subHours(new Date(), 12).toJSON()
+    );
     if (hasCooldownNow) {
-      return res.status(400).json({ error: 'Cooldown because of a malware URL. Wait 12h' });
+      throw new Error('Cooldown because of a malware URL. Wait 12h');
     }
   }
-  return next();
 };
 
-exports.malwareCheck = async ({ body, user }, res, next) => {
+exports.malwareCheck = async (user, target) => {
   const isMalware = await axios.post(
     `https://safebrowsing.googleapis.com/v4/threatMatches:find?key=${
       config.GOOGLE_SAFE_BROWSING_KEY
@@ -130,7 +136,7 @@ exports.malwareCheck = async ({ body, user }, res, next) => {
         ],
         platformTypes: ['ANY_PLATFORM', 'PLATFORM_TYPE_UNSPECIFIED'],
         threatEntryTypes: ['EXECUTABLE', 'URL', 'THREAT_ENTRY_TYPE_UNSPECIFIED'],
-        threatEntries: [{ url: body.target }],
+        threatEntries: [{ url: target }],
       },
     }
   );
@@ -138,9 +144,38 @@ exports.malwareCheck = async ({ body, user }, res, next) => {
     if (user) {
       await addCooldown(user);
     }
-    return res
-      .status(400)
-      .json({ error: user ? 'Malware detected! Cooldown for 12h.' : 'Malware detected!' });
+    throw new Error(user ? 'Malware detected! Cooldown for 12h.' : 'Malware detected!');
+  }
+};
+
+exports.urlCountsCheck = async email => {
+  const { count } = await urlCountFromDate({
+    email,
+    date: subDay(new Date(), 1).toJSON(),
+  });
+  if (count > config.USER_LIMIT_PER_DAY) {
+    throw new Error(
+      `You have reached your daily limit (${config.USER_LIMIT_PER_DAY}). Please wait 24h.`
+    );
+  }
+};
+
+exports.checkBannedDomain = async domain => {
+  const isDomainBanned = await getBannedDomain(domain);
+  if (isDomainBanned) {
+    throw new Error('URL is containing malware/scam.');
+  }
+};
+
+exports.checkBannedHost = async domain => {
+  let isHostBanned;
+  try {
+    const dnsRes = await dnsLookup(domain);
+    isHostBanned = await getBannedHost(dnsRes && dnsRes.address);
+  } catch (error) {
+    isHostBanned = null;
+  }
+  if (isHostBanned) {
+    throw new Error('URL is containing malware/scam.');
   }
-  return next();
 };

+ 5 - 3
server/db/user.js

@@ -9,7 +9,8 @@ exports.getUser = ({ email = '', apikey = '' }) =>
       .readTransaction(tx =>
         tx.run(
           'MATCH (u:USER) WHERE u.email = $email OR u.apikey = $apikey ' +
-            'OPTIONAL MATCH (u)-[:OWNS]->(l) RETURN u, l',
+            'OPTIONAL MATCH (u)-[r:RECEIVED]->(c) WITH u, collect(c.date) as cooldowns ' +
+            'OPTIONAL MATCH (u)-[:OWNS]->(d) RETURN u, d, cooldowns',
           {
             apikey,
             email,
@@ -19,11 +20,12 @@ exports.getUser = ({ email = '', apikey = '' }) =>
       .then(res => {
         session.close();
         const user = res.records.length && res.records[0].get('u').properties;
-        const domainProps = res.records.length && res.records[0].get('l');
+        const cooldowns = res.records.length && res.records[0].get('cooldowns');
+        const domainProps = res.records.length && res.records[0].get('d');
         const domain = domainProps ? domainProps.properties.name : '';
         const homepage = domainProps ? domainProps.properties.homepage : '';
         const useHttps = domainProps ? domainProps.properties.useHttps : '';
-        return resolve(user && { ...user, domain, homepage, useHttps });
+        return resolve(user && { ...user, cooldowns, domain, homepage, useHttps });
       })
       .catch(err => reject(err));
   });

+ 0 - 4
server/server.js

@@ -11,8 +11,6 @@ const {
   validateBody,
   validationCriterias,
   validateUrl,
-  cooldownCheck,
-  malwareCheck,
 } = require('./controllers/validateBodyController');
 const auth = require('./controllers/authController');
 const url = require('./controllers/urlController');
@@ -101,8 +99,6 @@ app.prepare().then(() => {
     auth.authJwtLoose,
     catchErrors(auth.recaptcha),
     catchErrors(validateUrl),
-    /* Allows running without Google Safe Browsing enabled */
-    config.GOOGLE_SAFE_BROWSING_KEY ? [catchErrors(cooldownCheck), catchErrors(malwareCheck)] : [],
     catchErrors(url.urlShortener)
   );
   server.post('/api/url/deleteurl', auth.authApikey, auth.authJwt, catchErrors(url.deleteUrl));