validators.js 14 KB

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