validateBodyController.ts 6.2 KB

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