validators.handler.js 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612
  1. const { isAfter, subDays, subHours, addMilliseconds, differenceInHours } = require("date-fns");
  2. const { body, param, query: queryValidator } = require("express-validator");
  3. const promisify = require("util").promisify;
  4. const bcrypt = require("bcryptjs");
  5. const dns = require("dns");
  6. const URL = require("url");
  7. const ms = require("ms");
  8. const { ROLES } = require("../consts");
  9. const query = require("../queries");
  10. const utils = require("../utils");
  11. const knex = require("../knex");
  12. const env = require("../env");
  13. const dnsLookup = promisify(dns.lookup);
  14. const checkUser = (value, { req }) => !!req.user;
  15. const sanitizeCheckbox = value => value === true || value === "on" || value;
  16. const createLink = [
  17. body("target")
  18. .exists({ checkNull: true, checkFalsy: true })
  19. .withMessage("Target is missing.")
  20. .isString()
  21. .trim()
  22. .isLength({ min: 1, max: 2040 })
  23. .withMessage("Maximum URL length is 2040.")
  24. .customSanitizer(utils.addProtocol)
  25. .custom(value => utils.urlRegex.test(value) || /^(?!https?|ftp)(\w+:|\/\/)/.test(value))
  26. .withMessage("URL is not valid.")
  27. .custom(value => utils.removeWww(URL.parse(value).host) !== env.DEFAULT_DOMAIN)
  28. .withMessage(`${env.DEFAULT_DOMAIN} URLs are not allowed.`),
  29. body("password")
  30. .optional({ nullable: true, checkFalsy: true })
  31. .custom(checkUser)
  32. .withMessage("Only users can use this field.")
  33. .isString()
  34. .isLength({ min: 3, max: 64 })
  35. .withMessage("Password length must be between 3 and 64."),
  36. body("customurl")
  37. .optional({ nullable: true, checkFalsy: true })
  38. .custom(checkUser)
  39. .withMessage("Only users can use this field.")
  40. .isString()
  41. .trim()
  42. .isLength({ min: 1, max: 64 })
  43. .withMessage("Custom URL length must be between 1 and 64.")
  44. .custom(value => /^[a-zA-Z0-9-_]+$/g.test(value))
  45. .withMessage("Custom URL is not valid.")
  46. .custom(value => !utils.preservedURLs.some(url => url.toLowerCase() === value))
  47. .withMessage("You can't use this custom URL."),
  48. body("reuse")
  49. .optional({ nullable: true })
  50. .custom(checkUser)
  51. .withMessage("Only users can use this field.")
  52. .isBoolean()
  53. .withMessage("Reuse must be boolean."),
  54. body("description")
  55. .optional({ nullable: true, checkFalsy: true })
  56. .isString()
  57. .trim()
  58. .isLength({ min: 1, max: 2040 })
  59. .withMessage("Description length must be between 1 and 2040."),
  60. body("expire_in")
  61. .optional({ nullable: true, checkFalsy: true })
  62. .isString()
  63. .trim()
  64. .custom(value => {
  65. try {
  66. return !!ms(value);
  67. } catch {
  68. return false;
  69. }
  70. })
  71. .withMessage("Expire format is invalid. Valid examples: 1m, 8h, 42 days.")
  72. .customSanitizer(ms)
  73. .custom(value => value >= ms("1m"))
  74. .withMessage("Expire time should be more than 1 minute.")
  75. .customSanitizer(value => utils.dateToUTC(addMilliseconds(new Date(), value))),
  76. body("domain")
  77. .optional({ nullable: true, checkFalsy: true })
  78. .customSanitizer(value => value === env.DEFAULT_DOMAIN ? null : value)
  79. .custom(checkUser)
  80. .withMessage("Only users can use this field.")
  81. .isString()
  82. .withMessage("Domain should be string.")
  83. .customSanitizer(value => value.toLowerCase())
  84. .custom(async (address, { req }) => {
  85. const domain = await query.domain.find({
  86. address,
  87. user_id: req.user.id
  88. });
  89. req.body.fetched_domain = domain || null;
  90. if (!domain) return Promise.reject();
  91. })
  92. .withMessage("You can't use this domain.")
  93. ];
  94. const editLink = [
  95. body("target")
  96. .optional({ checkFalsy: true, nullable: true })
  97. .isString()
  98. .trim()
  99. .isLength({ min: 1, max: 2040 })
  100. .withMessage("Maximum URL length is 2040.")
  101. .customSanitizer(utils.addProtocol)
  102. .custom(value => utils.urlRegex.test(value) || /^(?!https?|ftp)(\w+:|\/\/)/.test(value))
  103. .withMessage("URL is not valid.")
  104. .custom(value => utils.removeWww(URL.parse(value).host) !== env.DEFAULT_DOMAIN)
  105. .withMessage(`${env.DEFAULT_DOMAIN} URLs are not allowed.`),
  106. body("password")
  107. .optional({ nullable: true, checkFalsy: true })
  108. .isString()
  109. .isLength({ min: 3, max: 64 })
  110. .withMessage("Password length must be between 3 and 64."),
  111. body("address")
  112. .optional({ checkFalsy: true, nullable: true })
  113. .isString()
  114. .trim()
  115. .isLength({ min: 1, max: 64 })
  116. .withMessage("Custom URL length must be between 1 and 64.")
  117. .custom(value => /^[a-zA-Z0-9-_]+$/g.test(value))
  118. .withMessage("Custom URL is not valid")
  119. .custom(value => !utils.preservedURLs.some(url => url.toLowerCase() === value))
  120. .withMessage("You can't use this custom URL."),
  121. body("expire_in")
  122. .optional({ nullable: true, checkFalsy: true })
  123. .isString()
  124. .trim()
  125. .custom(value => {
  126. try {
  127. return !!ms(value);
  128. } catch {
  129. return false;
  130. }
  131. })
  132. .withMessage("Expire format is invalid. Valid examples: 1m, 8h, 42 days.")
  133. .customSanitizer(ms)
  134. .custom(value => value >= ms("1m"))
  135. .withMessage("Expire time should be more than 1 minute.")
  136. .customSanitizer(value => utils.dateToUTC(addMilliseconds(new Date(), value))),
  137. body("description")
  138. .optional({ nullable: true, checkFalsy: true })
  139. .isString()
  140. .trim()
  141. .isLength({ min: 0, max: 2040 })
  142. .withMessage("Description length must be between 0 and 2040."),
  143. param("id", "ID is invalid.")
  144. .exists({ checkFalsy: true, checkNull: true })
  145. .isLength({ min: 36, max: 36 })
  146. ];
  147. const redirectProtected = [
  148. body("password", "Password is invalid.")
  149. .exists({ checkFalsy: true, checkNull: true })
  150. .isString()
  151. .isLength({ min: 3, max: 64 })
  152. .withMessage("Password length must be between 3 and 64."),
  153. param("id", "ID is invalid.")
  154. .exists({ checkFalsy: true, checkNull: true })
  155. .isLength({ min: 36, max: 36 })
  156. ];
  157. const addDomain = [
  158. body("address", "Domain is not valid.")
  159. .exists({ checkFalsy: true, checkNull: true })
  160. .isLength({ min: 3, max: 64 })
  161. .withMessage("Domain length must be between 3 and 64.")
  162. .trim()
  163. .customSanitizer(utils.addProtocol)
  164. .custom(value => utils.urlRegex.test(value))
  165. .customSanitizer(value => {
  166. const parsed = URL.parse(value);
  167. return utils.removeWww(parsed.hostname || parsed.href);
  168. })
  169. .custom(value => value !== env.DEFAULT_DOMAIN)
  170. .withMessage("You can't use the default domain.")
  171. .custom(async value => {
  172. const domain = await query.domain.find({ address: value });
  173. if (domain?.user_id || domain?.banned) return Promise.reject();
  174. })
  175. .withMessage("You can't add this domain."),
  176. body("homepage")
  177. .optional({ checkFalsy: true, nullable: true })
  178. .customSanitizer(utils.addProtocol)
  179. .custom(value => utils.urlRegex.test(value) || /^(?!https?|ftp)(\w+:|\/\/)/.test(value))
  180. .withMessage("Homepage is not valid.")
  181. ];
  182. const addDomainAdmin = [
  183. body("address", "Domain is not valid.")
  184. .exists({ checkFalsy: true, checkNull: true })
  185. .isLength({ min: 3, max: 64 })
  186. .withMessage("Domain length must be between 3 and 64.")
  187. .trim()
  188. .customSanitizer(utils.addProtocol)
  189. .custom(value => utils.urlRegex.test(value))
  190. .customSanitizer(value => {
  191. const parsed = URL.parse(value);
  192. return utils.removeWww(parsed.hostname || parsed.href);
  193. })
  194. .custom(value => value !== env.DEFAULT_DOMAIN)
  195. .withMessage("You can't add the default domain.")
  196. .custom(async value => {
  197. const domain = await query.domain.find({ address: value });
  198. if (domain) return Promise.reject();
  199. })
  200. .withMessage("Domain already exists."),
  201. body("homepage")
  202. .optional({ checkFalsy: true, nullable: true })
  203. .customSanitizer(utils.addProtocol)
  204. .custom(value => utils.urlRegex.test(value) || /^(?!https?|ftp)(\w+:|\/\/)/.test(value))
  205. .withMessage("Homepage is not valid."),
  206. body("banned")
  207. .optional({ nullable: true })
  208. .customSanitizer(sanitizeCheckbox)
  209. .isBoolean(),
  210. ]
  211. const removeDomain = [
  212. param("id", "ID is invalid.")
  213. .exists({
  214. checkFalsy: true,
  215. checkNull: true
  216. })
  217. .isLength({ min: 36, max: 36 })
  218. ];
  219. const removeDomainAdmin = [
  220. param("id", "ID is invalid.")
  221. .exists({
  222. checkFalsy: true,
  223. checkNull: true
  224. })
  225. .isNumeric(),
  226. queryValidator("links")
  227. .optional({ nullable: true })
  228. .customSanitizer(sanitizeCheckbox)
  229. .isBoolean(),
  230. ];
  231. const deleteLink = [
  232. param("id", "ID is invalid.")
  233. .exists({
  234. checkFalsy: true,
  235. checkNull: true
  236. })
  237. .isLength({ min: 36, max: 36 })
  238. ];
  239. const reportLink = [
  240. body("link", "No link has been provided.")
  241. .exists({
  242. checkFalsy: true,
  243. checkNull: true
  244. })
  245. .customSanitizer(utils.addProtocol)
  246. .custom(
  247. value => utils.removeWww(URL.parse(value).host) === env.DEFAULT_DOMAIN
  248. )
  249. .withMessage(`You can only report a ${env.DEFAULT_DOMAIN} link.`)
  250. ];
  251. const banLink = [
  252. param("id", "ID is invalid.")
  253. .exists({
  254. checkFalsy: true,
  255. checkNull: true
  256. })
  257. .isLength({ min: 36, max: 36 }),
  258. body("host", '"host" should be a boolean.')
  259. .optional({
  260. nullable: true
  261. })
  262. .customSanitizer(sanitizeCheckbox)
  263. .isBoolean(),
  264. body("user", '"user" should be a boolean.')
  265. .optional({
  266. nullable: true
  267. })
  268. .customSanitizer(sanitizeCheckbox)
  269. .isBoolean(),
  270. body("userLinks", '"userLinks" should be a boolean.')
  271. .optional({
  272. nullable: true
  273. })
  274. .customSanitizer(sanitizeCheckbox)
  275. .isBoolean(),
  276. body("domain", '"domain" should be a boolean.')
  277. .optional({
  278. nullable: true
  279. })
  280. .customSanitizer(sanitizeCheckbox)
  281. .isBoolean()
  282. ];
  283. const banUser = [
  284. param("id", "ID is invalid.")
  285. .exists({
  286. checkFalsy: true,
  287. checkNull: true
  288. })
  289. .isNumeric(),
  290. body("links", '"links" should be a boolean.')
  291. .optional({
  292. nullable: true
  293. })
  294. .customSanitizer(sanitizeCheckbox)
  295. .isBoolean(),
  296. body("domains", '"domains" should be a boolean.')
  297. .optional({
  298. nullable: true
  299. })
  300. .customSanitizer(sanitizeCheckbox)
  301. .isBoolean()
  302. ];
  303. const banDomain = [
  304. param("id", "ID is invalid.")
  305. .exists({
  306. checkFalsy: true,
  307. checkNull: true
  308. })
  309. .isNumeric(),
  310. body("links", '"links" should be a boolean.')
  311. .optional({
  312. nullable: true
  313. })
  314. .customSanitizer(sanitizeCheckbox)
  315. .isBoolean(),
  316. body("domains", '"domains" should be a boolean.')
  317. .optional({
  318. nullable: true
  319. })
  320. .customSanitizer(sanitizeCheckbox)
  321. .isBoolean()
  322. ];
  323. const createUser = [
  324. body("password", "Password is not valid.")
  325. .exists({ checkFalsy: true, checkNull: true })
  326. .isLength({ min: 8, max: 64 })
  327. .withMessage("Password length must be between 8 and 64."),
  328. body("email", "Email is not valid.")
  329. .exists({ checkFalsy: true, checkNull: true })
  330. .trim()
  331. .isEmail()
  332. .isLength({ min: 0, max: 255 })
  333. .withMessage("Email length must be max 255.")
  334. .custom(async (value, { req }) => {
  335. const user = await query.user.find({ email: value });
  336. if (user)
  337. return Promise.reject();
  338. })
  339. .withMessage("User already exists."),
  340. body("role", "Role is not valid.")
  341. .optional({ nullable: true, checkFalsy: true })
  342. .trim()
  343. .isIn([ROLES.USER, ROLES.ADMIN]),
  344. body("verified")
  345. .optional({ nullable: true })
  346. .customSanitizer(sanitizeCheckbox)
  347. .isBoolean(),
  348. body("banned")
  349. .optional({ nullable: true })
  350. .customSanitizer(sanitizeCheckbox)
  351. .isBoolean(),
  352. body("verification_email")
  353. .optional({ nullable: true })
  354. .customSanitizer(sanitizeCheckbox)
  355. .isBoolean(),
  356. ];
  357. const getStats = [
  358. param("id", "ID is invalid.")
  359. .exists({
  360. checkFalsy: true,
  361. checkNull: true
  362. })
  363. .isLength({ min: 36, max: 36 })
  364. ];
  365. const signup = [
  366. body("password", "Password is not valid.")
  367. .exists({ checkFalsy: true, checkNull: true })
  368. .isLength({ min: 8, max: 64 })
  369. .withMessage("Password length must be between 8 and 64."),
  370. body("email", "Email is not valid.")
  371. .exists({ checkFalsy: true, checkNull: true })
  372. .trim()
  373. .isEmail()
  374. .isLength({ min: 0, max: 255 })
  375. .withMessage("Email length must be max 255.")
  376. .custom(async (value, { req }) => {
  377. const user = await query.user.find({ email: value });
  378. if (user)
  379. req.user = user;
  380. if (user?.verified)
  381. return Promise.reject();
  382. })
  383. .withMessage("You can't use this email address.")
  384. ];
  385. const login = [
  386. body("password", "Password is not valid.")
  387. .exists({ checkFalsy: true, checkNull: true })
  388. .isLength({ min: 8, max: 64 })
  389. .withMessage("Password length must be between 8 and 64."),
  390. body("email", "Email is not valid.")
  391. .exists({ checkFalsy: true, checkNull: true })
  392. .trim()
  393. .isEmail()
  394. .isLength({ min: 1, max: 255 })
  395. .withMessage("Email length must be max 255.")
  396. ];
  397. const changePassword = [
  398. body("currentpassword", "Password is not valid.")
  399. .exists({ checkFalsy: true, checkNull: true })
  400. .isLength({ min: 8, max: 64 })
  401. .withMessage("Password length must be between 8 and 64."),
  402. body("newpassword", "Password is not valid.")
  403. .exists({ checkFalsy: true, checkNull: true })
  404. .isLength({ min: 8, max: 64 })
  405. .withMessage("Password length must be between 8 and 64.")
  406. ];
  407. const changeEmail = [
  408. body("password", "Password is not valid.")
  409. .exists({ checkFalsy: true, checkNull: true })
  410. .isLength({ min: 8, max: 64 })
  411. .withMessage("Password length must be between 8 and 64."),
  412. body("email", "Email address is not valid.")
  413. .exists({ checkFalsy: true, checkNull: true })
  414. .trim()
  415. .isEmail()
  416. .isLength({ min: 1, max: 255 })
  417. .withMessage("Email length must be max 255.")
  418. ];
  419. const resetPassword = [
  420. body("email", "Email is not valid.")
  421. .exists({ checkFalsy: true, checkNull: true })
  422. .trim()
  423. .isEmail()
  424. .isLength({ min: 0, max: 255 })
  425. .withMessage("Email length must be max 255.")
  426. ];
  427. const deleteUser = [
  428. body("password", "Password is not valid.")
  429. .exists({ checkFalsy: true, checkNull: true })
  430. .isLength({ min: 8, max: 64 })
  431. .custom(async (password, { req }) => {
  432. const isMatch = await bcrypt.compare(password, req.user.password);
  433. if (!isMatch) return Promise.reject();
  434. })
  435. .withMessage("Password is not correct.")
  436. ];
  437. const deleteUserByAdmin = [
  438. param("id", "ID is invalid.")
  439. .exists({ checkFalsy: true, checkNull: true })
  440. .isNumeric()
  441. ];
  442. // TODO: if user has posted malware should do something better
  443. function cooldown(user) {
  444. if (!user?.cooldown) return;
  445. // If user has active cooldown then throw error
  446. const hasCooldownNow = differenceInHours(new Date(), utils.parseDatetime(user.cooldown)) < 12;
  447. if (hasCooldownNow) {
  448. throw new utils.CustomError("Cooldown because of a malware URL. Wait 12h");
  449. }
  450. }
  451. // TODO: if user or non-user has posted malware should do something better
  452. async function malware(user, target) {
  453. if (!env.GOOGLE_SAFE_BROWSING_KEY) return;
  454. const isMalware = await fetch(
  455. `https://safebrowsing.googleapis.com/v4/threatMatches:find?key=${env.GOOGLE_SAFE_BROWSING_KEY}`,
  456. {
  457. method: "post",
  458. body: JSON.stringify({
  459. client: {
  460. clientId: env.DEFAULT_DOMAIN.toLowerCase().replace(".", ""),
  461. clientVersion: "1.0.0"
  462. },
  463. threatInfo: {
  464. threatTypes: [
  465. "THREAT_TYPE_UNSPECIFIED",
  466. "MALWARE",
  467. "SOCIAL_ENGINEERING",
  468. "UNWANTED_SOFTWARE",
  469. "POTENTIALLY_HARMFUL_APPLICATION"
  470. ],
  471. platformTypes: ["ANY_PLATFORM", "PLATFORM_TYPE_UNSPECIFIED"],
  472. threatEntryTypes: [
  473. "EXECUTABLE",
  474. "URL",
  475. "THREAT_ENTRY_TYPE_UNSPECIFIED"
  476. ],
  477. threatEntries: [{ url: target }]
  478. }
  479. })
  480. }
  481. ).then(res => res.json());
  482. if (!isMalware.data || !isMalware.data.matches) return;
  483. if (user) {
  484. const [updatedUser] = await query.user.update(
  485. { id: user.id },
  486. { cooldown: utils.dateToUTC(new Date()) },
  487. { increments: ["malicious_attempts"] }
  488. );
  489. // Ban if too many cooldowns
  490. if (updatedUser.malicious_attempts > 2) {
  491. await query.user.update({ id: user.id }, { banned: true });
  492. throw new utils.CustomError("Too much malware requests. You are now banned.");
  493. }
  494. }
  495. throw new utils.CustomError(
  496. user ? "Malware detected! Cooldown for 12h." : "Malware detected!"
  497. );
  498. };
  499. async function linksCount(user) {
  500. if (!user) return;
  501. const count = await query.link.total({
  502. user_id: user.id,
  503. "links.created_at": [">", utils.dateToUTC(subDays(new Date(), 1))]
  504. });
  505. if (count > env.USER_LIMIT_PER_DAY) {
  506. throw new utils.CustomError(
  507. `You have reached your daily limit (${env.USER_LIMIT_PER_DAY}). Please wait 24h.`
  508. );
  509. }
  510. };
  511. async function bannedDomain(domain) {
  512. const isBanned = await query.domain.find({
  513. address: domain,
  514. banned: true
  515. });
  516. if (isBanned) {
  517. throw new utils.CustomError("Domain is banned.", 400);
  518. }
  519. };
  520. async function bannedHost(domain) {
  521. let isBanned;
  522. try {
  523. const dnsRes = await dnsLookup(domain);
  524. if (!dnsRes || !dnsRes.address) return;
  525. isBanned = await query.host.find({
  526. address: dnsRes.address,
  527. banned: true
  528. });
  529. } catch (error) {
  530. isBanned = null;
  531. }
  532. if (isBanned) {
  533. throw new utils.CustomError("URL is containing malware/scam.", 400);
  534. }
  535. };
  536. module.exports = {
  537. addDomain,
  538. addDomainAdmin,
  539. banDomain,
  540. banLink,
  541. banUser,
  542. bannedDomain,
  543. bannedHost,
  544. changeEmail,
  545. changePassword,
  546. checkUser,
  547. cooldown,
  548. createLink,
  549. createUser,
  550. deleteLink,
  551. deleteUser,
  552. deleteUserByAdmin,
  553. editLink,
  554. getStats,
  555. linksCount,
  556. login,
  557. malware,
  558. redirectProtected,
  559. removeDomain,
  560. removeDomainAdmin,
  561. reportLink,
  562. resetPassword,
  563. signup,
  564. }