links.handler.js 12 KB

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