validators.handler.js 14 KB

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