links.handler.js 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513
  1. const { differenceInSeconds } = require("date-fns");
  2. const promisify = require("util").promisify;
  3. const bcrypt = require("bcryptjs");
  4. const isbot = require("isbot");
  5. const URL = require("url");
  6. const dns = require("dns");
  7. const validators = require("./validators.handler");
  8. const map = require("../utils/map.json");
  9. const transporter = require("../mail");
  10. const query = require("../queries");
  11. const queue = require("../queues");
  12. const utils = require("../utils");
  13. const env = require("../env");
  14. const CustomError = utils.CustomError;
  15. const dnsLookup = promisify(dns.lookup);
  16. async function get(req, res) {
  17. const { limit, skip, all } = req.context;
  18. const search = req.query.search;
  19. const userId = req.user.id;
  20. const match = {
  21. ...(!all && { user_id: userId })
  22. };
  23. const [data, total] = await Promise.all([
  24. query.link.get(match, { limit, search, skip }),
  25. query.link.total(match, { search })
  26. ]);
  27. const links = data.map(utils.sanitize.link);
  28. if (req.isHTML) {
  29. res.render("partials/links/table", {
  30. total,
  31. limit,
  32. skip,
  33. links,
  34. })
  35. return;
  36. }
  37. return res.send({
  38. total,
  39. limit,
  40. skip,
  41. data: links,
  42. });
  43. };
  44. async function create(req, res) {
  45. const { reuse, password, customurl, description, target, fetched_domain, expire_in } = req.body;
  46. const domain_id = fetched_domain ? fetched_domain.id : null;
  47. const targetDomain = utils.removeWww(URL.parse(target).hostname);
  48. const tasks = await Promise.all([
  49. validators.cooldown(req.user),
  50. validators.malware(req.user, target),
  51. validators.linksCount(req.user),
  52. reuse &&
  53. query.link.find({
  54. target,
  55. user_id: req.user.id,
  56. domain_id
  57. }),
  58. customurl &&
  59. query.link.find({
  60. address: customurl,
  61. domain_id
  62. }),
  63. !customurl && utils.generateId(query, domain_id),
  64. validators.bannedDomain(targetDomain),
  65. validators.bannedHost(targetDomain)
  66. ]);
  67. // if "reuse" is true, try to return
  68. // the existent URL without creating one
  69. if (tasks[3]) {
  70. return res.json(utils.sanitize.link(tasks[3]));
  71. }
  72. // Check if custom link already exists
  73. if (tasks[4]) {
  74. const error = "Custom URL is already in use.";
  75. res.locals.errors = { customurl: error };
  76. throw new CustomError(error);
  77. }
  78. // Create new link
  79. const address = customurl || tasks[5];
  80. const link = await query.link.create({
  81. password,
  82. address,
  83. domain_id,
  84. description,
  85. target,
  86. expire_in,
  87. user_id: req.user && req.user.id
  88. });
  89. if (!req.user && env.NON_USER_COOLDOWN) {
  90. query.ip.add(req.realIP);
  91. }
  92. link.domain = fetched_domain?.address;
  93. if (req.isHTML) {
  94. res.setHeader("HX-Trigger", "reloadLinks");
  95. const shortURL = utils.getShortURL(link.address, link.domain);
  96. return res.render("partials/shortener", {
  97. link: shortURL.link,
  98. url: shortURL.url,
  99. });
  100. }
  101. return res
  102. .status(201)
  103. .send(utils.sanitize.link({ ...link }));
  104. }
  105. async function edit(req, res) {
  106. const link = await query.link.find({
  107. uuid: req.params.id,
  108. ...(!req.user.admin && { user_id: req.user.id })
  109. });
  110. if (!link) {
  111. throw new CustomError("Link was not found.");
  112. }
  113. let isChanged = false;
  114. [
  115. [req.body.address, "address"],
  116. [req.body.target, "target"],
  117. [req.body.description, "description"],
  118. [req.body.expire_in, "expire_in"],
  119. [req.body.password, "password"]
  120. ].forEach(([value, name]) => {
  121. if (!value) {
  122. if (name === "password" && link.password)
  123. req.body.password = null;
  124. else {
  125. delete req.body[name];
  126. return;
  127. }
  128. }
  129. if (value === link[name] && name !== "password") {
  130. delete req.body[name];
  131. return;
  132. }
  133. if (name === "expire_in")
  134. if (differenceInSeconds(new Date(value), new Date(link.expire_in)) <= 60)
  135. return;
  136. if (name === "password")
  137. if (value && value.replace(/•/ig, "").length === 0) {
  138. delete req.body.password;
  139. return;
  140. }
  141. isChanged = true;
  142. });
  143. if (!isChanged) {
  144. throw new CustomError("Should at least update one field.");
  145. }
  146. const { address, target, description, expire_in, password } = req.body;
  147. const targetDomain = target && utils.removeWww(URL.parse(target).hostname);
  148. const domain_id = link.domain_id || null;
  149. const tasks = await Promise.all([
  150. validators.cooldown(req.user),
  151. target && validators.malware(req.user, target),
  152. address &&
  153. query.link.find({
  154. address,
  155. domain_id
  156. }),
  157. target && validators.bannedDomain(targetDomain),
  158. target && validators.bannedHost(targetDomain)
  159. ]);
  160. // Check if custom link already exists
  161. if (tasks[2]) {
  162. const error = "Custom URL is already in use.";
  163. res.locals.errors = { address: error };
  164. throw new CustomError("Custom URL is already in use.");
  165. }
  166. // Update link
  167. const [updatedLink] = await query.link.update(
  168. {
  169. id: link.id
  170. },
  171. {
  172. ...(address && { address }),
  173. ...(description && { description }),
  174. ...(target && { target }),
  175. ...(expire_in && { expire_in }),
  176. ...((password || password === null) && { password })
  177. }
  178. );
  179. if (req.isHTML) {
  180. res.render("partials/links/edit", {
  181. swap_oob: true,
  182. success: "Link has been updated.",
  183. ...utils.sanitize.link({ ...link, ...updatedLink }),
  184. });
  185. return;
  186. }
  187. return res.status(200).send(utils.sanitize.link({ ...link, ...updatedLink }));
  188. };
  189. async function remove(req, res) {
  190. const { error, isRemoved, link } = await query.link.remove({
  191. uuid: req.params.id,
  192. ...(!req.user.admin && { user_id: req.user.id })
  193. });
  194. if (!isRemoved) {
  195. const messsage = error || "Could not delete the link.";
  196. throw new CustomError(messsage);
  197. }
  198. if (req.isHTML) {
  199. res.setHeader("HX-Reswap", "outerHTML");
  200. res.setHeader("HX-Trigger", "reloadLinks");
  201. res.render("partials/links/dialog/delete_success", {
  202. link: utils.getShortURL(link.address, link.domain).link,
  203. });
  204. return;
  205. }
  206. return res
  207. .status(200)
  208. .send({ message: "Link has been deleted successfully." });
  209. };
  210. async function report(req, res) {
  211. const { link } = req.body;
  212. await transporter.sendReportEmail(link);
  213. if (req.isHTML) {
  214. res.render("partials/report/form", {
  215. message: "Report was received. We'll take actions shortly."
  216. });
  217. return;
  218. }
  219. return res
  220. .status(200)
  221. .send({ message: "Thanks for the report, we'll take actions shortly." });
  222. };
  223. async function ban(req, res) {
  224. const { id } = req.params;
  225. const update = {
  226. banned_by_id: req.user.id,
  227. banned: true
  228. };
  229. // 1. Check if link exists
  230. const link = await query.link.find({ uuid: id });
  231. if (!link) {
  232. throw new CustomError("No link has been found.", 400);
  233. }
  234. if (link.banned) {
  235. throw new CustomError("Link has been banned already.", 400);
  236. }
  237. const tasks = [];
  238. // 2. Ban link
  239. tasks.push(query.link.update({ uuid: id }, update));
  240. const domain = utils.removeWww(URL.parse(link.target).hostname);
  241. // 3. Ban target's domain
  242. if (req.body.domain) {
  243. tasks.push(query.domain.add({ ...update, address: domain }));
  244. }
  245. // 4. Ban target's host
  246. if (req.body.host) {
  247. const dnsRes = await dnsLookup(domain).catch(() => {
  248. throw new CustomError("Couldn't fetch DNS info.");
  249. });
  250. const host = dnsRes?.address;
  251. tasks.push(query.host.add({ ...update, address: host }));
  252. }
  253. // 5. Ban link owner
  254. if (req.body.user && link.user_id) {
  255. tasks.push(query.user.update({ id: link.user_id }, update));
  256. }
  257. // 6. Ban all of owner's links
  258. if (req.body.userLinks && link.user_id) {
  259. tasks.push(query.link.update({ user_id: link.user_id }, update));
  260. }
  261. // 7. Wait for all tasks to finish
  262. await Promise.all(tasks).catch((err) => {
  263. throw new CustomError("Couldn't ban entries.");
  264. });
  265. // 8. Send response
  266. if (req.isHTML) {
  267. res.setHeader("HX-Reswap", "outerHTML");
  268. res.setHeader("HX-Trigger", "reloadLinks");
  269. res.render("partials/links/dialog/ban_success", {
  270. link: utils.getShortURL(link.address, link.domain).link,
  271. });
  272. return;
  273. }
  274. return res.status(200).send({ message: "Banned link successfully." });
  275. };
  276. async function redirect(req, res, next) {
  277. const isPreservedUrl = utils.preservedURLs.some(
  278. item => item === req.path.replace("/", "")
  279. );
  280. if (isPreservedUrl) return next();
  281. // 1. If custom domain, get domain info
  282. const host = utils.removeWww(req.headers.host);
  283. const domain =
  284. host !== env.DEFAULT_DOMAIN
  285. ? await query.domain.find({ address: host })
  286. : null;
  287. // 2. Get link
  288. const address = req.params.id.replace("+", "");
  289. const link = await query.link.find({
  290. address,
  291. domain_id: domain ? domain.id : null
  292. });
  293. // 3. When no link, if has domain redirect to domain's homepage
  294. // otherwise redirect to 404
  295. if (!link) {
  296. return res.redirect(domain?.homepage || "/404");
  297. }
  298. // 4. If link is banned, redirect to banned page.
  299. if (link.banned) {
  300. return res.redirect("/banned");
  301. }
  302. // 5. If wants to see link info, then redirect
  303. const isRequestingInfo = /.*\+$/gi.test(req.params.id);
  304. if (isRequestingInfo && !link.password) {
  305. if (req.isHTML) {
  306. res.render("url_info", {
  307. title: "Short link information",
  308. target: link.target,
  309. link: utils.getShortURL(link.address, link.domain).link
  310. });
  311. return;
  312. }
  313. return res.send({ target: link.target });
  314. }
  315. // 6. If link is protected, redirect to password page
  316. if (link.password) {
  317. res.render("protected", {
  318. title: "Protected short link",
  319. id: link.uuid
  320. });
  321. return;
  322. }
  323. // 7. Create link visit
  324. const isBot = isbot(req.headers["user-agent"]);
  325. if (link.user_id && !isBot) {
  326. queue.visit.add({
  327. headers: req.headers,
  328. realIP: req.realIP,
  329. referrer: req.get("Referrer"),
  330. link
  331. });
  332. }
  333. // 8. Redirect to target
  334. return res.redirect(link.target);
  335. };
  336. async function redirectProtected(req, res) {
  337. // 1. Get link
  338. const uuid = req.params.id;
  339. const link = await query.link.find({ uuid });
  340. // 2. Throw error if no link
  341. if (!link || !link.password) {
  342. throw new CustomError("Couldn't find the link.", 400);
  343. }
  344. // 3. Check if password matches
  345. const matches = await bcrypt.compare(req.body.password, link.password);
  346. if (!matches) {
  347. throw new CustomError("Password is not correct.", 401);
  348. }
  349. // 4. Create visit
  350. if (link.user_id) {
  351. queue.visit.add({
  352. headers: req.headers,
  353. realIP: req.realIP,
  354. referrer: req.get("Referrer"),
  355. link
  356. });
  357. }
  358. // 5. Send target
  359. if (req.isHTML) {
  360. res.setHeader("HX-Redirect", link.target);
  361. res.render("partials/protected/form", {
  362. id: link.uuid,
  363. message: "Redirecting...",
  364. });
  365. return;
  366. }
  367. return res.status(200).send({ target: link.target });
  368. };
  369. async function redirectCustomDomainHomepage(req, res, next) {
  370. const path = req.path;
  371. const host = utils.removeWww(req.headers.host);
  372. if (host === env.DEFAULT_DOMAIN) {
  373. return next();
  374. }
  375. if (
  376. path === "/" ||
  377. utils.preservedURLs
  378. .filter(l => l !== "url-password")
  379. .some(item => item === path.replace("/", ""))
  380. ) {
  381. const domain = await query.domain.find({ address: host });
  382. const redirectURL = domain
  383. ? domain.homepage
  384. : `https://${env.DEFAULT_DOMAIN + path}`;
  385. return res.redirect(302, redirectURL);
  386. }
  387. return next();
  388. };
  389. async function stats(req, res) {
  390. const { user } = req;
  391. const uuid = req.params.id;
  392. const link = await query.link.find({
  393. ...(!user.admin && { user_id: user.id }),
  394. uuid
  395. });
  396. if (!link) {
  397. if (req.isHTML) {
  398. res.setHeader("HX-Redirect", "/404");
  399. res.status(200).send("");
  400. return;
  401. }
  402. throw new CustomError("Link could not be found.");
  403. }
  404. const stats = await query.visit.find({ link_id: link.id }, link.visit_count);
  405. if (!stats) {
  406. throw new CustomError("Could not get the short link stats. Try again later.");
  407. }
  408. if (req.isHTML) {
  409. res.render("partials/stats", {
  410. link: utils.sanitize.link(link),
  411. stats,
  412. map,
  413. });
  414. return;
  415. }
  416. return res.status(200).send({
  417. ...stats,
  418. ...utils.sanitize.link(link)
  419. });
  420. };
  421. module.exports = {
  422. ban,
  423. create,
  424. edit,
  425. get,
  426. remove,
  427. report,
  428. stats,
  429. redirect,
  430. redirectProtected,
  431. redirectCustomDomainHomepage,
  432. }