validateBodyController.ts 6.3 KB

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