validateBodyController.ts 6.2 KB

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