validators.handler.js 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626
  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 createAdmin = [
  398. body("password", "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("email", "Email is not valid.")
  403. .exists({ checkFalsy: true, checkNull: true })
  404. .trim()
  405. .isEmail()
  406. .isLength({ min: 0, max: 255 })
  407. .withMessage("Email length must be max 255.")
  408. ];
  409. const changePassword = [
  410. body("currentpassword", "Password is not valid.")
  411. .exists({ checkFalsy: true, checkNull: true })
  412. .isLength({ min: 8, max: 64 })
  413. .withMessage("Password length must be between 8 and 64."),
  414. body("newpassword", "Password is not valid.")
  415. .exists({ checkFalsy: true, checkNull: true })
  416. .isLength({ min: 8, max: 64 })
  417. .withMessage("Password length must be between 8 and 64.")
  418. ];
  419. const changeEmail = [
  420. body("password", "Password is not valid.")
  421. .exists({ checkFalsy: true, checkNull: true })
  422. .isLength({ min: 8, max: 64 })
  423. .withMessage("Password length must be between 8 and 64."),
  424. body("email", "Email address is not valid.")
  425. .exists({ checkFalsy: true, checkNull: true })
  426. .trim()
  427. .isEmail()
  428. .isLength({ min: 1, max: 255 })
  429. .withMessage("Email length must be max 255.")
  430. ];
  431. const resetPassword = [
  432. body("email", "Email is not valid.")
  433. .exists({ checkFalsy: true, checkNull: true })
  434. .trim()
  435. .isEmail()
  436. .isLength({ min: 0, max: 255 })
  437. .withMessage("Email length must be max 255.")
  438. ];
  439. const deleteUser = [
  440. body("password", "Password is not valid.")
  441. .exists({ checkFalsy: true, checkNull: true })
  442. .isLength({ min: 8, max: 64 })
  443. .custom(async (password, { req }) => {
  444. const isMatch = await bcrypt.compare(password, req.user.password);
  445. if (!isMatch) return Promise.reject();
  446. })
  447. .withMessage("Password is not correct.")
  448. ];
  449. const deleteUserByAdmin = [
  450. param("id", "ID is invalid.")
  451. .exists({ checkFalsy: true, checkNull: true })
  452. .isNumeric()
  453. ];
  454. // TODO: if user has posted malware should do something better
  455. function cooldown(user) {
  456. if (!user?.cooldown) return;
  457. // If user has active cooldown then throw error
  458. const hasCooldownNow = differenceInHours(new Date(), utils.parseDatetime(user.cooldown)) < 12;
  459. if (hasCooldownNow) {
  460. throw new utils.CustomError("Cooldown because of a malware URL. Wait 12h");
  461. }
  462. }
  463. // TODO: if user or non-user has posted malware should do something better
  464. async function malware(user, target) {
  465. if (!env.GOOGLE_SAFE_BROWSING_KEY) return;
  466. const isMalware = await fetch(
  467. `https://safebrowsing.googleapis.com/v4/threatMatches:find?key=${env.GOOGLE_SAFE_BROWSING_KEY}`,
  468. {
  469. method: "post",
  470. body: JSON.stringify({
  471. client: {
  472. clientId: env.DEFAULT_DOMAIN.toLowerCase().replace(".", ""),
  473. clientVersion: "1.0.0"
  474. },
  475. threatInfo: {
  476. threatTypes: [
  477. "THREAT_TYPE_UNSPECIFIED",
  478. "MALWARE",
  479. "SOCIAL_ENGINEERING",
  480. "UNWANTED_SOFTWARE",
  481. "POTENTIALLY_HARMFUL_APPLICATION"
  482. ],
  483. platformTypes: ["ANY_PLATFORM", "PLATFORM_TYPE_UNSPECIFIED"],
  484. threatEntryTypes: [
  485. "EXECUTABLE",
  486. "URL",
  487. "THREAT_ENTRY_TYPE_UNSPECIFIED"
  488. ],
  489. threatEntries: [{ url: target }]
  490. }
  491. })
  492. }
  493. ).then(res => res.json());
  494. if (!isMalware.data || !isMalware.data.matches) return;
  495. if (user) {
  496. const [updatedUser] = await query.user.update(
  497. { id: user.id },
  498. { cooldown: utils.dateToUTC(new Date()) },
  499. { increments: ["malicious_attempts"] }
  500. );
  501. // Ban if too many cooldowns
  502. if (updatedUser.malicious_attempts > 2) {
  503. await query.user.update({ id: user.id }, { banned: true });
  504. throw new utils.CustomError("Too much malware requests. You are now banned.");
  505. }
  506. }
  507. throw new utils.CustomError(
  508. user ? "Malware detected! Cooldown for 12h." : "Malware detected!"
  509. );
  510. };
  511. async function linksCount(user) {
  512. if (!user) return;
  513. const count = await query.link.total({
  514. user_id: user.id,
  515. "links.created_at": [">", utils.dateToUTC(subDays(new Date(), 1))]
  516. });
  517. if (count > env.USER_LIMIT_PER_DAY) {
  518. throw new utils.CustomError(
  519. `You have reached your daily limit (${env.USER_LIMIT_PER_DAY}). Please wait 24h.`
  520. );
  521. }
  522. };
  523. async function bannedDomain(domain) {
  524. const isBanned = await query.domain.find({
  525. address: domain,
  526. banned: true
  527. });
  528. if (isBanned) {
  529. throw new utils.CustomError("Domain is banned.", 400);
  530. }
  531. };
  532. async function bannedHost(domain) {
  533. let isBanned;
  534. try {
  535. const dnsRes = await dnsLookup(domain);
  536. if (!dnsRes || !dnsRes.address) return;
  537. isBanned = await query.host.find({
  538. address: dnsRes.address,
  539. banned: true
  540. });
  541. } catch (error) {
  542. isBanned = null;
  543. }
  544. if (isBanned) {
  545. throw new utils.CustomError("URL is containing malware/scam.", 400);
  546. }
  547. };
  548. module.exports = {
  549. addDomain,
  550. addDomainAdmin,
  551. banDomain,
  552. banLink,
  553. banUser,
  554. bannedDomain,
  555. bannedHost,
  556. changeEmail,
  557. changePassword,
  558. checkUser,
  559. cooldown,
  560. createAdmin,
  561. createLink,
  562. createUser,
  563. deleteLink,
  564. deleteUser,
  565. deleteUserByAdmin,
  566. editLink,
  567. getStats,
  568. linksCount,
  569. login,
  570. malware,
  571. redirectProtected,
  572. removeDomain,
  573. removeDomainAdmin,
  574. reportLink,
  575. resetPassword,
  576. signup,
  577. }