validators.handler.js 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475
  1. const { body, param } = require("express-validator");
  2. const { isAfter, subDays, subHours, addMilliseconds } = require("date-fns");
  3. const urlRegex = require("url-regex-safe");
  4. const { promisify } = require("util");
  5. const bcrypt = require("bcryptjs");
  6. const axios = require("axios");
  7. const dns = require("dns");
  8. const URL = require("url");
  9. const ms = require("ms");
  10. const { CustomError, addProtocol, preservedURLs, removeWww } = require("../utils");
  11. const query = require("../queries");
  12. const knex = require("../knex");
  13. const env = require("../env");
  14. const dnsLookup = promisify(dns.lookup);
  15. const checkUser = (value, { req }) => !!req.user;
  16. let body1;
  17. let body2;
  18. const createLink = [
  19. body("target")
  20. .exists({ checkNull: true, checkFalsy: true })
  21. .withMessage("Target is missing.")
  22. .isString()
  23. .trim()
  24. .isLength({ min: 1, max: 2040 })
  25. .withMessage("Maximum URL length is 2040.")
  26. .customSanitizer(addProtocol)
  27. .custom(
  28. value =>
  29. urlRegex({ exact: true, strict: false }).test(value) ||
  30. /^(?!https?)(\w+):\/\//.test(value)
  31. )
  32. .withMessage("URL is not valid.")
  33. .custom(value => removeWww(URL.parse(value).host) !== env.DEFAULT_DOMAIN)
  34. .withMessage(`${env.DEFAULT_DOMAIN} URLs are not allowed.`),
  35. body("password")
  36. .optional({ nullable: true, checkFalsy: true })
  37. .custom(checkUser)
  38. .withMessage("Only users can use this field.")
  39. .isString()
  40. .isLength({ min: 3, max: 64 })
  41. .withMessage("Password length must be between 3 and 64."),
  42. body("customurl")
  43. .optional({ nullable: true, checkFalsy: true })
  44. .custom(checkUser)
  45. .withMessage("Only users can use this field.")
  46. .isString()
  47. .trim()
  48. .isLength({ min: 1, max: 64 })
  49. .withMessage("Custom URL length must be between 1 and 64.")
  50. .custom(value => /^[a-zA-Z0-9-_]+$/g.test(value))
  51. .withMessage("Custom URL is not valid.")
  52. .custom(value => !preservedURLs.some(url => url.toLowerCase() === value))
  53. .withMessage("You can't use this custom URL."),
  54. body("reuse")
  55. .optional({ nullable: true })
  56. .custom(checkUser)
  57. .withMessage("Only users can use this field.")
  58. .isBoolean()
  59. .withMessage("Reuse must be boolean."),
  60. body("description")
  61. .optional({ nullable: true, checkFalsy: true })
  62. .isString()
  63. .trim()
  64. .isLength({ min: 1, max: 2040 })
  65. .withMessage("Description length must be between 1 and 2040."),
  66. body("expire_in")
  67. .optional({ nullable: true, checkFalsy: true })
  68. .isString()
  69. .trim()
  70. .custom(value => {
  71. try {
  72. return !!ms(value);
  73. } catch {
  74. return false;
  75. }
  76. })
  77. .withMessage("Expire format is invalid. Valid examples: 1m, 8h, 42 days.")
  78. .customSanitizer(ms)
  79. .custom(value => value >= ms("1m"))
  80. .withMessage("Expire time should be more than 1 minute.")
  81. .customSanitizer(value => addMilliseconds(new Date(), value).toISOString()),
  82. body("domain")
  83. .optional({ nullable: true, checkFalsy: true })
  84. .custom(checkUser)
  85. .withMessage("Only users can use this field.")
  86. .isString()
  87. .withMessage("Domain should be string.")
  88. .customSanitizer(value => value.toLowerCase())
  89. .custom(async (address, { req }) => {
  90. if (address === env.DEFAULT_DOMAIN) {
  91. req.body.domain = null;
  92. return;
  93. }
  94. const domain = await query.domain.find({
  95. address,
  96. user_id: req.user.id
  97. });
  98. req.body.fetched_domain = domain || null;
  99. if (!domain) return Promise.reject();
  100. })
  101. .withMessage("You can't use this domain.")
  102. ];
  103. const editLink = [
  104. body("target")
  105. .optional({ checkFalsy: true, nullable: true })
  106. .isString()
  107. .trim()
  108. .isLength({ min: 1, max: 2040 })
  109. .withMessage("Maximum URL length is 2040.")
  110. .customSanitizer(addProtocol)
  111. .custom(
  112. value =>
  113. urlRegex({ exact: true, strict: false }).test(value) ||
  114. /^(?!https?)(\w+):\/\//.test(value)
  115. )
  116. .withMessage("URL is not valid.")
  117. .custom(value => removeWww(URL.parse(value).host) !== env.DEFAULT_DOMAIN)
  118. .withMessage(`${env.DEFAULT_DOMAIN} URLs are not allowed.`),
  119. body("password")
  120. .optional({ nullable: true, checkFalsy: true })
  121. .isString()
  122. .isLength({ min: 3, max: 64 })
  123. .withMessage("Password length must be between 3 and 64."),
  124. body("address")
  125. .optional({ checkFalsy: true, nullable: true })
  126. .isString()
  127. .trim()
  128. .isLength({ min: 1, max: 64 })
  129. .withMessage("Custom URL length must be between 1 and 64.")
  130. .custom(value => /^[a-zA-Z0-9-_]+$/g.test(value))
  131. .withMessage("Custom URL is not valid")
  132. .custom(value => !preservedURLs.some(url => url.toLowerCase() === value))
  133. .withMessage("You can't use this custom URL."),
  134. body("expire_in")
  135. .optional({ nullable: true, checkFalsy: true })
  136. .isString()
  137. .trim()
  138. .custom(value => {
  139. try {
  140. return !!ms(value);
  141. } catch {
  142. return false;
  143. }
  144. })
  145. .withMessage("Expire format is invalid. Valid examples: 1m, 8h, 42 days.")
  146. .customSanitizer(ms)
  147. .custom(value => value >= ms("1m"))
  148. .withMessage("Expire time should be more than 1 minute.")
  149. .customSanitizer(value => addMilliseconds(new Date(), value).toISOString()),
  150. body("description")
  151. .optional({ nullable: true, checkFalsy: true })
  152. .isString()
  153. .trim()
  154. .isLength({ min: 0, max: 2040 })
  155. .withMessage("Description length must be between 0 and 2040."),
  156. param("id", "ID is invalid.")
  157. .exists({ checkFalsy: true, checkNull: true })
  158. .isLength({ min: 36, max: 36 })
  159. ];
  160. // export const redirectProtected = [
  161. // body("password", "Password is invalid.")
  162. // .exists({ checkFalsy: true, checkNull: true })
  163. // .isString()
  164. // .isLength({ min: 3, max: 64 })
  165. // .withMessage("Password length must be between 3 and 64."),
  166. // param("id", "ID is invalid.")
  167. // .exists({ checkFalsy: true, checkNull: true })
  168. // .isLength({ min: 36, max: 36 })
  169. // ];
  170. // export const addDomain = [
  171. // body("address", "Domain is not valid")
  172. // .exists({ checkFalsy: true, checkNull: true })
  173. // .isLength({ min: 3, max: 64 })
  174. // .withMessage("Domain length must be between 3 and 64.")
  175. // .trim()
  176. // .customSanitizer(value => {
  177. // const parsed = URL.parse(value);
  178. // return removeWww(parsed.hostname || parsed.href);
  179. // })
  180. // .custom(value => urlRegex({ exact: true, strict: false }).test(value))
  181. // .custom(value => value !== env.DEFAULT_DOMAIN)
  182. // .withMessage("You can't use the default domain.")
  183. // .custom(async value => {
  184. // const domain = await query.domain.find({ address: value });
  185. // if (domain?.user_id || domain?.banned) return Promise.reject();
  186. // })
  187. // .withMessage("You can't add this domain."),
  188. // body("homepage")
  189. // .optional({ checkFalsy: true, nullable: true })
  190. // .customSanitizer(addProtocol)
  191. // .custom(value => urlRegex({ exact: true, strict: false }).test(value))
  192. // .withMessage("Homepage is not valid.")
  193. // ];
  194. // export const removeDomain = [
  195. // param("id", "ID is invalid.")
  196. // .exists({
  197. // checkFalsy: true,
  198. // checkNull: true
  199. // })
  200. // .isLength({ min: 36, max: 36 })
  201. // ];
  202. const deleteLink = [
  203. param("id", "ID is invalid.")
  204. .exists({
  205. checkFalsy: true,
  206. checkNull: true
  207. })
  208. .isLength({ min: 36, max: 36 })
  209. ];
  210. // export const reportLink = [
  211. // body("link", "No link has been provided.")
  212. // .exists({
  213. // checkFalsy: true,
  214. // checkNull: true
  215. // })
  216. // .customSanitizer(addProtocol)
  217. // .custom(
  218. // value => removeWww(URL.parse(value).hostname) === env.DEFAULT_DOMAIN
  219. // )
  220. // .withMessage(`You can only report a ${env.DEFAULT_DOMAIN} link.`)
  221. // ];
  222. // export const banLink = [
  223. // param("id", "ID is invalid.")
  224. // .exists({
  225. // checkFalsy: true,
  226. // checkNull: true
  227. // })
  228. // .isLength({ min: 36, max: 36 }),
  229. // body("host", '"host" should be a boolean.')
  230. // .optional({
  231. // nullable: true
  232. // })
  233. // .isBoolean(),
  234. // body("user", '"user" should be a boolean.')
  235. // .optional({
  236. // nullable: true
  237. // })
  238. // .isBoolean(),
  239. // body("userlinks", '"userlinks" should be a boolean.')
  240. // .optional({
  241. // nullable: true
  242. // })
  243. // .isBoolean(),
  244. // body("domain", '"domain" should be a boolean.')
  245. // .optional({
  246. // nullable: true
  247. // })
  248. // .isBoolean()
  249. // ];
  250. // export const getStats = [
  251. // param("id", "ID is invalid.")
  252. // .exists({
  253. // checkFalsy: true,
  254. // checkNull: true
  255. // })
  256. // .isLength({ min: 36, max: 36 })
  257. // ];
  258. const signup = [
  259. body("password", "Password is not valid.")
  260. .exists({ checkFalsy: true, checkNull: true })
  261. .isLength({ min: 8, max: 64 })
  262. .withMessage("Password length must be between 8 and 64."),
  263. body("email", "Email is not valid.")
  264. .exists({ checkFalsy: true, checkNull: true })
  265. .trim()
  266. .isEmail()
  267. .isLength({ min: 0, max: 255 })
  268. .withMessage("Email length must be max 255.")
  269. .custom(async (value, { req }) => {
  270. const user = await query.user.find({ email: value });
  271. if (user) {
  272. req.user = user;
  273. }
  274. if (user?.verified) return Promise.reject();
  275. })
  276. .withMessage("You can't use this email address.")
  277. ];
  278. const login = [
  279. body("password", "Password is not valid.")
  280. .exists({ checkFalsy: true, checkNull: true })
  281. .isLength({ min: 8, max: 64 })
  282. .withMessage("Password length must be between 8 and 64."),
  283. body("email", "Email is not valid.")
  284. .exists({ checkFalsy: true, checkNull: true })
  285. .trim()
  286. .isEmail()
  287. .isLength({ min: 0, max: 255 })
  288. .withMessage("Email length must be max 255.")
  289. ];
  290. // export const changePassword = [
  291. // body("password", "Password is not valid.")
  292. // .exists({ checkFalsy: true, checkNull: true })
  293. // .isLength({ min: 8, max: 64 })
  294. // .withMessage("Password length must be between 8 and 64.")
  295. // ];
  296. // export const resetPasswordRequest = [
  297. // body("email", "Email is not valid.")
  298. // .exists({ checkFalsy: true, checkNull: true })
  299. // .trim()
  300. // .isEmail()
  301. // .isLength({ min: 0, max: 255 })
  302. // .withMessage("Email length must be max 255."),
  303. // body("password", "Password is not valid.")
  304. // .exists({ checkFalsy: true, checkNull: true })
  305. // .isLength({ min: 8, max: 64 })
  306. // .withMessage("Password length must be between 8 and 64.")
  307. // ];
  308. // export const resetEmailRequest = [
  309. // body("email", "Email is not valid.")
  310. // .exists({ checkFalsy: true, checkNull: true })
  311. // .trim()
  312. // .isEmail()
  313. // .isLength({ min: 0, max: 255 })
  314. // .withMessage("Email length must be max 255.")
  315. // ];
  316. // export const deleteUser = [
  317. // body("password", "Password is not valid.")
  318. // .exists({ checkFalsy: true, checkNull: true })
  319. // .isLength({ min: 8, max: 64 })
  320. // .custom(async (password, { req }) => {
  321. // const isMatch = await bcrypt.compare(password, req.user.password);
  322. // if (!isMatch) return Promise.reject();
  323. // })
  324. // ];
  325. // TODO: if user has posted malware should do something better
  326. function cooldown(user) {
  327. if (!env.GOOGLE_SAFE_BROWSING_KEY || !user || !user.cooldowns) return;
  328. // If has active cooldown then throw error
  329. const hasCooldownNow = user.cooldowns.some(cooldown =>
  330. isAfter(subHours(new Date(), 12), new Date(cooldown))
  331. );
  332. if (hasCooldownNow) {
  333. throw new CustomError("Cooldown because of a malware URL. Wait 12h");
  334. }
  335. }
  336. // TODO: if user or non-user has posted malware should do something better
  337. async function malware(user, target) {
  338. if (!env.GOOGLE_SAFE_BROWSING_KEY) return;
  339. const isMalware = await axios.post(
  340. `https://safebrowsing.googleapis.com/v4/threatMatches:find?key=${env.GOOGLE_SAFE_BROWSING_KEY}`,
  341. {
  342. client: {
  343. clientId: env.DEFAULT_DOMAIN.toLowerCase().replace(".", ""),
  344. clientVersion: "1.0.0"
  345. },
  346. threatInfo: {
  347. threatTypes: [
  348. "THREAT_TYPE_UNSPECIFIED",
  349. "MALWARE",
  350. "SOCIAL_ENGINEERING",
  351. "UNWANTED_SOFTWARE",
  352. "POTENTIALLY_HARMFUL_APPLICATION"
  353. ],
  354. platformTypes: ["ANY_PLATFORM", "PLATFORM_TYPE_UNSPECIFIED"],
  355. threatEntryTypes: [
  356. "EXECUTABLE",
  357. "URL",
  358. "THREAT_ENTRY_TYPE_UNSPECIFIED"
  359. ],
  360. threatEntries: [{ url: target }]
  361. }
  362. }
  363. );
  364. if (!isMalware.data || !isMalware.data.matches) return;
  365. if (user) {
  366. const [updatedUser] = await query.user.update(
  367. { id: user.id },
  368. {
  369. cooldowns: knex.raw("array_append(cooldowns, ?)", [
  370. new Date().toISOString()
  371. ])
  372. }
  373. );
  374. // Ban if too many cooldowns
  375. if (updatedUser.cooldowns.length > 2) {
  376. await query.user.update({ id: user.id }, { banned: true });
  377. throw new CustomError("Too much malware requests. You are now banned.");
  378. }
  379. }
  380. throw new CustomError(
  381. user ? "Malware detected! Cooldown for 12h." : "Malware detected!"
  382. );
  383. };
  384. async function linksCount(user) {
  385. if (!user) return;
  386. const count = await query.link.total({
  387. user_id: user.id,
  388. "links.created_at": [">", subDays(new Date(), 1).toISOString()]
  389. });
  390. if (count > env.USER_LIMIT_PER_DAY) {
  391. throw new CustomError(
  392. `You have reached your daily limit (${env.USER_LIMIT_PER_DAY}). Please wait 24h.`
  393. );
  394. }
  395. };
  396. async function bannedDomain(domain) {
  397. const isBanned = await query.domain.find({
  398. address: domain,
  399. banned: true
  400. });
  401. if (isBanned) {
  402. throw new CustomError("URL is containing malware/scam.", 400);
  403. }
  404. };
  405. async function bannedHost(domain) {
  406. let isBanned;
  407. try {
  408. const dnsRes = await dnsLookup(domain);
  409. if (!dnsRes || !dnsRes.address) return;
  410. isBanned = await query.host.find({
  411. address: dnsRes.address,
  412. banned: true
  413. });
  414. } catch (error) {
  415. isBanned = null;
  416. }
  417. if (isBanned) {
  418. throw new CustomError("URL is containing malware/scam.", 400);
  419. }
  420. };
  421. module.exports = {
  422. bannedDomain,
  423. bannedHost,
  424. checkUser,
  425. cooldown,
  426. createLink,
  427. deleteLink,
  428. editLink,
  429. linksCount,
  430. login,
  431. malware,
  432. signup,
  433. }