Просмотр исходного кода

Seperate validation from creating new URL handler

Pouria Ezzati 7 лет назад
Родитель
Сommit
a4d884896a

+ 6 - 59
server/controllers/urlController.js

@@ -3,7 +3,6 @@ const URL = require('url');
 const useragent = require('useragent');
 const geoip = require('geoip-lite');
 const bcrypt = require('bcryptjs');
-const axios = require('axios');
 const {
   createShortUrl,
   createVisit,
@@ -15,48 +14,12 @@ const {
   deleteCustomDomain,
   deleteUrl,
 } = require('../db/url');
+const { addProtocol } = require('../utils');
 const config = require('../config');
 
-const preservedUrls = [
-  'login',
-  'logout',
-  'signup',
-  'reset-password',
-  'resetpassword',
-  'url-password',
-  'settings',
-  'stats',
-  'verify',
-  'api',
-  '404',
-  'static',
-  'images',
-];
-
-exports.preservedUrls = preservedUrls;
-
 exports.urlShortener = async ({ body, user }, res) => {
-  if (!body.target) return res.status(400).json({ error: 'No target has been provided.' });
-  if (body.target.length > 1024) {
-    return res.status(400).json({ error: 'Maximum URL length is 1024.' });
-  }
-  const isValidUrl = urlRegex({ exact: true, strict: false }).test(body.target);
-  if (!isValidUrl) return res.status(400).json({ error: 'URL is not valid.' });
-  const hasProtocol = /^https?/.test(URL.parse(body.target).protocol);
-  const target = hasProtocol ? body.target : `http://${body.target}`;
-  if (body.password && body.password.length > 64) {
-    return res.status(400).json({ error: 'Maximum password length is 64.' });
-  }
+  // Check if custom URL already exists
   if (user && body.customurl) {
-    if (!/^[a-zA-Z1-9-_]+$/g.test(body.customurl.trim())) {
-      return res.status(400).json({ error: 'Custom URL is not valid.' });
-    }
-    if (preservedUrls.some(url => url === body.customurl)) {
-      return res.status(400).json({ error: "You can't use this custom URL name." });
-    }
-    if (body.customurl.length > 64) {
-      return res.status(400).json({ error: 'Maximum custom URL length is 64.' });
-    }
     const urls = await findUrl({ id: body.customurl || '' });
     if (urls.length) {
       const urlWithNoDomain = !user.domain && urls.some(url => !url.domain);
@@ -66,27 +29,11 @@ exports.urlShortener = async ({ body, user }, res) => {
       }
     }
   }
-  const isMalware = await axios.post(
-    `https://safebrowsing.googleapis.com/v4/threatMatches:find?key=${
-      config.GOOGLE_SAFE_BROWSING_KEY
-    }`,
-    {
-      client: {
-        clientId: config.DEFAULT_DOMAIN.toLowerCase().replace('.', ''),
-        clientVersion: '1.0.0',
-      },
-      threatInfo: {
-        threatTypes: ['MALWARE', 'SOCIAL_ENGINEERING'],
-        platformTypes: ['WINDOWS'],
-        threatEntryTypes: ['URL'],
-        threatEntries: [{ url: body.target }],
-      },
-    }
-  );
-  if (isMalware.data && isMalware.data.matches) {
-    return res.status(400).json({ error: 'Malware detected!' });
-  }
+
+  // Create new URL
+  const target = addProtocol(body.target);
   const url = await createShortUrl({ ...body, target, user });
+
   return res.json(url);
 };
 

+ 89 - 3
server/controllers/validateBodyController.js

@@ -1,15 +1,20 @@
-const { body } = require('express-validator/check');
+const axios = require('axios');
+const urlRegex = require('url-regex');
+const validator = require('express-validator/check');
 const { validationResult } = require('express-validator/check');
+const config = require('../config');
 
 exports.validationCriterias = [
-  body('email')
+  validator
+    .body('email')
     .exists()
     .withMessage('Email must be provided.')
     .isEmail()
     .withMessage('Email is not valid.')
     .trim()
     .normalizeEmail(),
-  body('password', 'Password must be at least 8 chars long.')
+  validator
+    .body('password', 'Password must be at least 8 chars long.')
     .exists()
     .withMessage('Password must be provided.')
     .isLength({ min: 8 }),
@@ -25,3 +30,84 @@ exports.validateBody = (req, res, next) => {
   }
   return next();
 };
+
+const preservedUrls = [
+  'login',
+  'logout',
+  'signup',
+  'reset-password',
+  'resetpassword',
+  'url-password',
+  'settings',
+  'stats',
+  'verify',
+  'api',
+  '404',
+  'static',
+  'images',
+];
+
+exports.preservedUrls = preservedUrls;
+
+exports.validateUrl = async ({ body, user }, res, next) => {
+  // Validate URL existence
+  if (!body.target) return res.status(400).json({ error: 'No target has been provided.' });
+
+  // validate URL length
+  if (body.target.length > 1024) {
+    return res.status(400).json({ error: 'Maximum URL length is 1024.' });
+  }
+
+  // Validate URL
+  const isValidUrl = urlRegex({ exact: true, strict: false }).test(body.target);
+  if (!isValidUrl) return res.status(400).json({ error: 'URL is not valid.' });
+
+  // Validate password length
+  if (body.password && body.password.length > 64) {
+    return res.status(400).json({ error: 'Maximum password length is 64.' });
+  }
+
+  // Custom URL validations
+  if (user && body.customurl) {
+    // Validate custom URL
+    if (!/^[a-zA-Z1-9-_]+$/g.test(body.customurl.trim())) {
+      return res.status(400).json({ error: 'Custom URL is not valid.' });
+    }
+
+    // Prevent from using preserved URLs
+    if (preservedUrls.some(url => url === body.customurl)) {
+      return res.status(400).json({ error: "You can't use this custom URL name." });
+    }
+
+    // Validate custom URL length
+    if (body.customurl.length > 64) {
+      return res.status(400).json({ error: 'Maximum custom URL length is 64.' });
+    }
+  }
+
+  return next();
+};
+
+exports.malwareCheck = async ({ body }, res, next) => {
+  const isMalware = await axios.post(
+    `https://safebrowsing.googleapis.com/v4/threatMatches:find?key=${
+      config.GOOGLE_SAFE_BROWSING_KEY
+    }`,
+    {
+      client: {
+        clientId: config.DEFAULT_DOMAIN.toLowerCase().replace('.', ''),
+        clientVersion: '1.0.0',
+      },
+      threatInfo: {
+        threatTypes: ['MALWARE', 'SOCIAL_ENGINEERING'],
+        platformTypes: ['WINDOWS'],
+        threatEntryTypes: ['URL'],
+        threatEntries: [{ url: body.target }],
+      },
+    }
+  );
+  if (isMalware.data && isMalware.data.matches) {
+    return res.status(400).json({ error: 'Malware detected!' });
+  }
+  return next();
+};

+ 10 - 2
server/server.js

@@ -6,7 +6,13 @@ const Raven = require('raven');
 const cookieParser = require('cookie-parser');
 const bodyParser = require('body-parser');
 const passport = require('passport');
-const { validateBody, validationCriterias } = require('./controllers/validateBodyController');
+const {
+  validateBody,
+  validationCriterias,
+  preservedUrls,
+  validateUrl,
+  malwareCheck,
+} = require('./controllers/validateBodyController');
 const auth = require('./controllers/authController');
 const url = require('./controllers/urlController');
 const config = require('./config');
@@ -56,7 +62,7 @@ app.prepare().then(() => {
     const { headers, path } = req;
     if (
       headers.host !== config.DEFAULT_DOMAIN &&
-      (path === '/' || url.preservedUrls.some(item => item === path.replace('/', '')))
+      (path === '/' || preservedUrls.some(item => item === path.replace('/', '')))
     ) {
       return res.redirect(`http://${config.DEFAULT_DOMAIN + path}`);
     }
@@ -93,6 +99,8 @@ app.prepare().then(() => {
     auth.authApikey,
     auth.authJwtLoose,
     catchErrors(auth.recaptcha),
+    catchErrors(validateUrl),
+    catchErrors(malwareCheck),
     catchErrors(url.urlShortener)
   );
   server.post('/api/url/deleteurl', auth.authApikey, auth.authJwt, catchErrors(url.deleteUrl));

+ 6 - 0
server/utils/index.js

@@ -0,0 +1,6 @@
+const URL = require('url');
+
+exports.addProtocol = url => {
+  const hasProtocol = /^https?/.test(URL.parse(url).protocol);
+  return hasProtocol ? url : `http://${url}`;
+};