validateBodyController.js 5.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202
  1. const { promisify } = require('util');
  2. const dns = require('dns');
  3. const axios = require('axios');
  4. const URL = require('url');
  5. const urlRegex = require('url-regex');
  6. const validator = require('express-validator/check');
  7. const { differenceInMinutes, subHours } = require('date-fns/');
  8. const { validationResult } = require('express-validator/check');
  9. const { addCooldown, banUser, getIPCooldown: getIPCooldownCount } = require('../db/user');
  10. const { getBannedDomain, getBannedHost, urlCountFromDate } = require('../db/url');
  11. const subDay = require('date-fns/sub_days');
  12. const { addProtocol } = require('../utils');
  13. const dnsLookup = promisify(dns.lookup);
  14. exports.validationCriterias = [
  15. validator
  16. .body('email')
  17. .exists()
  18. .withMessage('Email must be provided.')
  19. .isEmail()
  20. .withMessage('Email is not valid.')
  21. .trim()
  22. .normalizeEmail(),
  23. validator
  24. .body('password', 'Password must be at least 8 chars long.')
  25. .exists()
  26. .withMessage('Password must be provided.')
  27. .isLength({ min: 8 }),
  28. ];
  29. exports.validateBody = (req, res, next) => {
  30. const errors = validationResult(req);
  31. if (!errors.isEmpty()) {
  32. const errorsObj = errors.mapped();
  33. const emailError = errorsObj.email && errorsObj.email.msg;
  34. const passwordError = errorsObj.password && errorsObj.password.msg;
  35. return res.status(400).json({ error: emailError || passwordError });
  36. }
  37. return next();
  38. };
  39. const preservedUrls = [
  40. 'login',
  41. 'logout',
  42. 'signup',
  43. 'reset-password',
  44. 'resetpassword',
  45. 'url-password',
  46. 'url-info',
  47. 'settings',
  48. 'stats',
  49. 'verify',
  50. 'api',
  51. '404',
  52. 'static',
  53. 'images',
  54. 'banned',
  55. 'terms',
  56. 'privacy',
  57. 'report',
  58. ];
  59. exports.preservedUrls = preservedUrls;
  60. exports.validateUrl = async ({ body, user }, res, next) => {
  61. // Validate URL existence
  62. if (!body.target) return res.status(400).json({ error: 'No target has been provided.' });
  63. // validate URL length
  64. if (body.target.length > 3000) {
  65. return res.status(400).json({ error: 'Maximum URL length is 3000.' });
  66. }
  67. // Validate URL
  68. const isValidUrl = urlRegex({ exact: true, strict: false }).test(body.target);
  69. if (!isValidUrl && !/^\w+:\/\//.test(body.target))
  70. return res.status(400).json({ error: 'URL is not valid.' });
  71. // If target is the URL shortener itself
  72. const { host } = URL.parse(addProtocol(body.target));
  73. if (host === process.env.DEFAULT_DOMAIN) {
  74. return res.status(400).json({ error: `${process.env.DEFAULT_DOMAIN} URLs are not allowed.` });
  75. }
  76. // Validate password length
  77. if (body.password && body.password.length > 64) {
  78. return res.status(400).json({ error: 'Maximum password length is 64.' });
  79. }
  80. // Custom URL validations
  81. if (user && body.customurl) {
  82. // Validate custom URL
  83. if (!/^[a-zA-Z0-9-_]+$/g.test(body.customurl.trim())) {
  84. return res.status(400).json({ error: 'Custom URL is not valid.' });
  85. }
  86. // Prevent from using preserved URLs
  87. if (preservedUrls.some(url => url === body.customurl)) {
  88. return res.status(400).json({ error: "You can't use this custom URL name." });
  89. }
  90. // Validate custom URL length
  91. if (body.customurl.length > 64) {
  92. return res.status(400).json({ error: 'Maximum custom URL length is 64.' });
  93. }
  94. }
  95. return next();
  96. };
  97. exports.cooldownCheck = async user => {
  98. if (user && user.cooldowns) {
  99. if (user.cooldowns.length > 4) {
  100. await banUser(user);
  101. throw new Error('Too much malware requests. You are now banned.');
  102. }
  103. const hasCooldownNow = user.cooldowns.some(
  104. cooldown => cooldown > subHours(new Date(), 12).toJSON()
  105. );
  106. if (hasCooldownNow) {
  107. throw new Error('Cooldown because of a malware URL. Wait 12h');
  108. }
  109. }
  110. };
  111. exports.ipCooldownCheck = async (req, res, next) => {
  112. const cooldonwConfig = Number(process.env.NON_USER_COOLDOWN);
  113. if (req.user || !cooldonwConfig) return next();
  114. const cooldownDate = await getIPCooldownCount(req.realIp);
  115. if (cooldownDate) {
  116. const timeToWait = cooldonwConfig - differenceInMinutes(new Date(), cooldownDate);
  117. return res
  118. .status(400)
  119. .json({ error: `Non-logged in users are limited. Wait ${timeToWait} minutes or log in.` });
  120. }
  121. next();
  122. };
  123. exports.malwareCheck = async (user, target) => {
  124. const isMalware = await axios.post(
  125. `https://safebrowsing.googleapis.com/v4/threatMatches:find?key=${
  126. process.env.GOOGLE_SAFE_BROWSING_KEY
  127. }`,
  128. {
  129. client: {
  130. clientId: process.env.DEFAULT_DOMAIN.toLowerCase().replace('.', ''),
  131. clientVersion: '1.0.0',
  132. },
  133. threatInfo: {
  134. threatTypes: [
  135. 'THREAT_TYPE_UNSPECIFIED',
  136. 'MALWARE',
  137. 'SOCIAL_ENGINEERING',
  138. 'UNWANTED_SOFTWARE',
  139. 'POTENTIALLY_HARMFUL_APPLICATION',
  140. ],
  141. platformTypes: ['ANY_PLATFORM', 'PLATFORM_TYPE_UNSPECIFIED'],
  142. threatEntryTypes: ['EXECUTABLE', 'URL', 'THREAT_ENTRY_TYPE_UNSPECIFIED'],
  143. threatEntries: [{ url: target }],
  144. },
  145. }
  146. );
  147. if (isMalware.data && isMalware.data.matches) {
  148. if (user) {
  149. await addCooldown(user);
  150. }
  151. throw new Error(user ? 'Malware detected! Cooldown for 12h.' : 'Malware detected!');
  152. }
  153. };
  154. exports.urlCountsCheck = async email => {
  155. const { count } = await urlCountFromDate({
  156. email,
  157. date: subDay(new Date(), 1).toJSON(),
  158. });
  159. if (count > Number(process.env.USER_LIMIT_PER_DAY)) {
  160. throw new Error(
  161. `You have reached your daily limit (${process.env.USER_LIMIT_PER_DAY}). Please wait 24h.`
  162. );
  163. }
  164. };
  165. exports.checkBannedDomain = async domain => {
  166. const isDomainBanned = await getBannedDomain(domain);
  167. if (isDomainBanned) {
  168. throw new Error('URL is containing malware/scam.');
  169. }
  170. };
  171. exports.checkBannedHost = async domain => {
  172. let isHostBanned;
  173. try {
  174. const dnsRes = await dnsLookup(domain);
  175. isHostBanned = await getBannedHost(dnsRes && dnsRes.address);
  176. } catch (error) {
  177. isHostBanned = null;
  178. }
  179. if (isHostBanned) {
  180. throw new Error('URL is containing malware/scam.');
  181. }
  182. };