links.ts 8.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359
  1. import ua from "universal-analytics";
  2. import { Handler } from "express";
  3. import { promisify } from "util";
  4. import bcrypt from "bcryptjs";
  5. import isbot from "isbot";
  6. import next from "next";
  7. import URL from "url";
  8. import dns from "dns";
  9. import * as validators from "./validators";
  10. import { CreateLinkReq } from "./types";
  11. import { CustomError } from "../utils";
  12. import transporter from "../mail/mail";
  13. import * as utils from "../utils";
  14. import query from "../queries";
  15. import queue from "../queues";
  16. import env from "../env";
  17. const dnsLookup = promisify(dns.lookup);
  18. export const get: Handler = async (req, res) => {
  19. const { limit, skip, search, all } = req.query;
  20. const userId = req.user.id;
  21. const match = {
  22. ...(!all && { user_id: userId })
  23. };
  24. const [links, total] = await Promise.all([
  25. query.link.get(match, { limit, search, skip }),
  26. query.link.total(match, { search })
  27. ]);
  28. const data = links.map(utils.sanitize.link);
  29. return res.send({
  30. total,
  31. limit,
  32. skip,
  33. data
  34. });
  35. };
  36. export const create: Handler = async (req: CreateLinkReq, res) => {
  37. const { reuse, password, customurl, target, domain } = req.body;
  38. const domain_id = domain ? domain.id : null;
  39. const targetDomain = URL.parse(target).hostname;
  40. const queries = await Promise.all([
  41. validators.cooldown(req.user),
  42. validators.malware(req.user, target),
  43. validators.linksCount(req.user),
  44. reuse &&
  45. query.link.find({
  46. target,
  47. user_id: req.user.id,
  48. domain_id
  49. }),
  50. customurl &&
  51. query.link.find({
  52. address: customurl,
  53. user_id: req.user.id,
  54. domain_id
  55. }),
  56. !customurl && utils.generateId(domain_id),
  57. validators.bannedDomain(targetDomain),
  58. validators.bannedHost(targetDomain)
  59. ]);
  60. // if "reuse" is true, try to return
  61. // the existent URL without creating one
  62. if (queries[3]) {
  63. return res.json(utils.sanitize.link(queries[3]));
  64. }
  65. // Check if custom link already exists
  66. if (queries[4]) {
  67. throw new CustomError("Custom URL is already in use.");
  68. }
  69. // Create new link
  70. const address = customurl || queries[5];
  71. const link = await query.link.create({
  72. password,
  73. address,
  74. domain_id,
  75. target,
  76. user_id: req.user && req.user.id
  77. });
  78. if (!req.user && env.NON_USER_COOLDOWN) {
  79. query.ip.add(req.realIP);
  80. }
  81. return res
  82. .status(201)
  83. .send(utils.sanitize.link({ ...link, domain: domain?.address }));
  84. };
  85. export const remove: Handler = async (req, res) => {
  86. const link = await query.link.remove({
  87. uuid: req.params.id,
  88. ...(!req.user.admin && { user_id: req.user.id })
  89. });
  90. if (!link) {
  91. throw new CustomError("Could not delete the link");
  92. }
  93. return res
  94. .status(200)
  95. .send({ message: "Link has been deleted successfully." });
  96. };
  97. export const report: Handler = async (req, res) => {
  98. const { link } = req.body;
  99. const mail = await transporter.sendMail({
  100. from: env.MAIL_USER,
  101. to: env.REPORT_MAIL,
  102. subject: "[REPORT]",
  103. text: link,
  104. html: link
  105. });
  106. if (!mail.accepted.length) {
  107. throw new CustomError("Couldn't submit the report. Try again later.");
  108. }
  109. return res
  110. .status(200)
  111. .send({ message: "Thanks for the report, we'll take actions shortly." });
  112. };
  113. export const ban: Handler = async (req, res) => {
  114. const { id } = req.params;
  115. const update = {
  116. banned_by_id: req.user.id,
  117. banned: true
  118. };
  119. // 1. Check if link exists
  120. const link = await query.link.find({ uuid: id });
  121. if (!link) {
  122. throw new CustomError("No link has been found.", 400);
  123. }
  124. if (link.banned) {
  125. return res.status(200).send({ message: "Link has been banned already." });
  126. }
  127. const tasks = [];
  128. // 2. Ban link
  129. tasks.push(query.link.update({ uuid: id }, update));
  130. const domain = URL.parse(link.target).hostname;
  131. // 3. Ban target's domain
  132. if (req.body.domain) {
  133. tasks.push(query.domain.add({ ...update, address: domain }));
  134. }
  135. // 4. Ban target's host
  136. if (req.body.host) {
  137. const dnsRes = await dnsLookup(domain).catch(() => {
  138. throw new CustomError("Couldn't fetch DNS info.");
  139. });
  140. const host = dnsRes?.address;
  141. tasks.push(query.host.add({ ...update, address: host }));
  142. }
  143. // 5. Ban link owner
  144. if (req.body.user) {
  145. tasks.push(query.user.update({ id: link.user_id }, update));
  146. }
  147. // 6. Ban all of owner's links
  148. if (req.body.userLinks) {
  149. tasks.push(query.link.update({ user_id: link.user_id }, update));
  150. }
  151. // 7. Wait for all tasks to finish
  152. await Promise.all(tasks).catch(() => {
  153. throw new CustomError("Couldn't ban entries.");
  154. });
  155. // 8. Send response
  156. return res.status(200).send({ message: "Banned link successfully." });
  157. };
  158. export const redirect = (app: ReturnType<typeof next>): Handler => async (
  159. req,
  160. res,
  161. next
  162. ) => {
  163. const isBot = isbot(req.headers["user-agent"]);
  164. const isPreservedUrl = validators.preservedUrls.some(
  165. item => item === req.path.replace("/", "")
  166. );
  167. if (isPreservedUrl) return next();
  168. // 1. If custom domain, get domain info
  169. const { host } = req.headers;
  170. const domain =
  171. host !== env.DEFAULT_DOMAIN
  172. ? await query.domain.find({ address: host })
  173. : null;
  174. // 2. Get link
  175. const address = req.params.id.replace("+", "");
  176. const link = await query.link.find({
  177. address,
  178. domain_id: domain && domain.id
  179. });
  180. // 3. When no link, if has domain redirect to domain's homepage
  181. // otherwise rediredt to 404
  182. if (!link) {
  183. return res.redirect(301, domain ? domain.homepage : "/404");
  184. }
  185. // 4. If link is banned, redirect to banned page.
  186. if (link.banned) {
  187. return res.redirect("/banned");
  188. }
  189. // 5. If wants to see link info, then redirect
  190. const doesRequestInfo = /.*\+$/gi.test(req.params.id);
  191. if (doesRequestInfo && !link.password) {
  192. return app.render(req, res, "/url-info", { target: link.target });
  193. }
  194. // 6. If link is protected, redirect to password page
  195. if (link.password) {
  196. return res.redirect(`/protected/${link.uuid}`);
  197. }
  198. // 7. Create link visit
  199. if (link.user_id && !isBot) {
  200. queue.visit.add({
  201. headers: req.headers,
  202. realIP: req.realIP,
  203. referrer: req.get("Referrer"),
  204. link
  205. });
  206. }
  207. // 8. Create Google Analytics visit
  208. if (env.GOOGLE_ANALYTICS_UNIVERSAL && !isBot) {
  209. ua(env.GOOGLE_ANALYTICS_UNIVERSAL)
  210. .pageview({
  211. dp: `/${address}`,
  212. ua: req.headers["user-agent"],
  213. uip: req.realIP,
  214. aip: 1
  215. })
  216. .send();
  217. }
  218. // 10. Redirect to target
  219. return res.redirect(link.target);
  220. };
  221. export const redirectProtected: Handler = async (req, res) => {
  222. // 1. Get link
  223. const uuid = req.params.id;
  224. const link = await query.link.find({ uuid });
  225. // 2. Throw error if no link
  226. if (!link || !link.password) {
  227. throw new CustomError("Couldn't find the link.", 400);
  228. }
  229. // 3. Check if password matches
  230. const matches = await bcrypt.compare(req.body.password, link.password);
  231. if (!matches) {
  232. throw new CustomError("Password is not correct.", 401);
  233. }
  234. // 4. Create visit
  235. if (link.user_id) {
  236. queue.visit.add({
  237. headers: req.headers,
  238. realIP: req.realIP,
  239. referrer: req.get("Referrer"),
  240. link
  241. });
  242. }
  243. // 5. Create Google Analytics visit
  244. if (env.GOOGLE_ANALYTICS_UNIVERSAL) {
  245. ua(env.GOOGLE_ANALYTICS_UNIVERSAL)
  246. .pageview({
  247. dp: `/${link.address}`,
  248. ua: req.headers["user-agent"],
  249. uip: req.realIP,
  250. aip: 1
  251. })
  252. .send();
  253. }
  254. // 6. Send target
  255. return res.status(200).send({ target: link.target });
  256. };
  257. export const redirectCustomDomain: Handler = async (req, res, next) => {
  258. const {
  259. headers: { host },
  260. path
  261. } = req;
  262. if (host === env.DEFAULT_DOMAIN) {
  263. return next();
  264. }
  265. if (
  266. path === "/" ||
  267. validators.preservedUrls
  268. .filter(l => l !== "url-password")
  269. .some(item => item === path.replace("/", ""))
  270. ) {
  271. const domain = await query.domain.find({ address: host });
  272. const redirectURL = domain
  273. ? domain.homepage
  274. : `https://${env.DEFAULT_DOMAIN + path}`;
  275. return res.redirect(301, redirectURL);
  276. }
  277. };
  278. export const stats: Handler = async (req, res) => {
  279. const { user } = req;
  280. const uuid = req.params.id;
  281. const link = await query.link.find({
  282. ...(!user.admin && { user_id: user.id }),
  283. uuid
  284. });
  285. if (!link) {
  286. throw new CustomError("Link could not be found.");
  287. }
  288. const stats = await query.visit.find({ link_id: link.id }, link.visit_count);
  289. if (!stats) {
  290. throw new CustomError("Could not get the short link stats.");
  291. }
  292. return res.status(200).send({
  293. ...stats,
  294. ...utils.sanitize.link(link)
  295. });
  296. };