validateBodyController.ts 6.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221
  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 { body } from "express-validator";
  8. import { differenceInMinutes, subHours, subDays, isAfter } from "date-fns";
  9. import { validationResult } from "express-validator";
  10. import { addCooldown, banUser } from "../db/user";
  11. import { getIP } from "../db/ip";
  12. import { getUserLinksCount } from "../db/link";
  13. import { getDomain } from "../db/domain";
  14. import { getHost } from "../db/host";
  15. import { addProtocol } from "../utils";
  16. const dnsLookup = promisify(dns.lookup);
  17. export const validationCriterias = [
  18. body("email")
  19. .exists()
  20. .withMessage("Email must be provided.")
  21. .isEmail()
  22. .withMessage("Email is not valid.")
  23. .trim(),
  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. export const 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. export 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. "pricing"
  59. ];
  60. export const validateUrl: RequestHandler = async (req, res, next) => {
  61. // Validate URL existence
  62. if (!req.body.target)
  63. return res.status(400).json({ error: "No target has been provided." });
  64. // validate URL length
  65. if (req.body.target.length > 2040) {
  66. return res.status(400).json({ error: "Maximum URL length is 2040." });
  67. }
  68. // Validate URL
  69. const isValidUrl = urlRegex({ exact: true, strict: false }).test(
  70. req.body.target
  71. );
  72. if (!isValidUrl && !/^\w+:\/\//.test(req.body.target))
  73. return res.status(400).json({ error: "URL is not valid." });
  74. // If target is the URL shortener itself
  75. const { host } = URL.parse(addProtocol(req.body.target));
  76. if (host === process.env.DEFAULT_DOMAIN) {
  77. return res
  78. .status(400)
  79. .json({ error: `${process.env.DEFAULT_DOMAIN} URLs are not allowed.` });
  80. }
  81. // Validate password length
  82. if (req.body.password && req.body.password.length > 64) {
  83. return res.status(400).json({ error: "Maximum password length is 64." });
  84. }
  85. // Custom URL validations
  86. if (req.user && req.body.customurl) {
  87. // Validate custom URL
  88. if (!/^[a-zA-Z0-9-_]+$/g.test(req.body.customurl.trim())) {
  89. return res.status(400).json({ error: "Custom URL is not valid." });
  90. }
  91. // Prevent from using preserved URLs
  92. if (preservedUrls.some(url => url === req.body.customurl)) {
  93. return res
  94. .status(400)
  95. .json({ error: "You can't use this custom URL name." });
  96. }
  97. // Validate custom URL length
  98. if (req.body.customurl.length > 64) {
  99. return res
  100. .status(400)
  101. .json({ error: "Maximum custom URL length is 64." });
  102. }
  103. }
  104. return next();
  105. };
  106. export const cooldownCheck = async (user: User) => {
  107. if (user && user.cooldowns) {
  108. if (user.cooldowns.length > 4) {
  109. await banUser(user.id);
  110. throw new Error("Too much malware requests. You are banned.");
  111. }
  112. const hasCooldownNow = user.cooldowns.some(cooldown =>
  113. isAfter(subHours(new Date(), 12), new Date(cooldown))
  114. );
  115. if (hasCooldownNow) {
  116. throw new Error("Cooldown because of a malware URL. Wait 12h");
  117. }
  118. }
  119. };
  120. export const ipCooldownCheck: RequestHandler = async (req, res, next) => {
  121. const cooldownConfig = Number(process.env.NON_USER_COOLDOWN);
  122. if (req.user || !cooldownConfig) return next();
  123. const ip = await getIP(req.realIP);
  124. if (ip) {
  125. const timeToWait =
  126. cooldownConfig - differenceInMinutes(new Date(), new Date(ip.created_at));
  127. return res.status(400).json({
  128. error:
  129. `Non-logged in users are limited. Wait ${timeToWait} ` +
  130. "minutes or log in."
  131. });
  132. }
  133. next();
  134. };
  135. export const malwareCheck = async (user: User, target: string) => {
  136. const isMalware = await axios.post(
  137. `https://safebrowsing.googleapis.com/v4/threatMatches:find?key=${process.env.GOOGLE_SAFE_BROWSING_KEY}`,
  138. {
  139. client: {
  140. clientId: process.env.DEFAULT_DOMAIN.toLowerCase().replace(".", ""),
  141. clientVersion: "1.0.0"
  142. },
  143. threatInfo: {
  144. threatTypes: [
  145. "THREAT_TYPE_UNSPECIFIED",
  146. "MALWARE",
  147. "SOCIAL_ENGINEERING",
  148. "UNWANTED_SOFTWARE",
  149. "POTENTIALLY_HARMFUL_APPLICATION"
  150. ],
  151. platformTypes: ["ANY_PLATFORM", "PLATFORM_TYPE_UNSPECIFIED"],
  152. threatEntryTypes: [
  153. "EXECUTABLE",
  154. "URL",
  155. "THREAT_ENTRY_TYPE_UNSPECIFIED"
  156. ],
  157. threatEntries: [{ url: target }]
  158. }
  159. }
  160. );
  161. if (isMalware.data && isMalware.data.matches) {
  162. if (user) {
  163. await addCooldown(user.id);
  164. }
  165. throw new Error(
  166. user ? "Malware detected! Cooldown for 12h." : "Malware detected!"
  167. );
  168. }
  169. };
  170. export const urlCountsCheck = async (user: User) => {
  171. const count = await getUserLinksCount({
  172. user_id: user.id,
  173. date: subDays(new Date(), 1)
  174. });
  175. if (count > Number(process.env.USER_LIMIT_PER_DAY)) {
  176. throw new Error(
  177. `You have reached your daily limit (${process.env.USER_LIMIT_PER_DAY}). Please wait 24h.`
  178. );
  179. }
  180. };
  181. export const checkBannedDomain = async (domain: string) => {
  182. const bannedDomain = await getDomain({ address: domain, banned: true });
  183. if (bannedDomain) {
  184. throw new Error("URL is containing malware/scam.");
  185. }
  186. };
  187. export const checkBannedHost = async (domain: string) => {
  188. let isHostBanned;
  189. try {
  190. const dnsRes = await dnsLookup(domain);
  191. isHostBanned = await getHost({
  192. address: dnsRes && dnsRes.address,
  193. banned: true
  194. });
  195. } catch (error) {
  196. isHostBanned = null;
  197. }
  198. if (isHostBanned) {
  199. throw new Error("URL is containing malware/scam.");
  200. }
  201. };