links.ts 10.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420
  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 {
  38. reuse,
  39. password,
  40. customurl,
  41. description,
  42. target,
  43. domain,
  44. expire_in
  45. } = req.body;
  46. const domain_id = domain ? 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(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. throw new CustomError("Custom URL is already in use.");
  75. }
  76. // Create new link
  77. const address = customurl || queries[5];
  78. const link = await query.link.create({
  79. password,
  80. address,
  81. domain_id,
  82. description,
  83. target,
  84. expire_in,
  85. user_id: req.user && req.user.id
  86. });
  87. if (!req.user && env.NON_USER_COOLDOWN) {
  88. query.ip.add(req.realIP);
  89. }
  90. return res
  91. .status(201)
  92. .send(utils.sanitize.link({ ...link, domain: domain?.address }));
  93. };
  94. export const edit: Handler = async (req, res) => {
  95. const { address, target, description, expire_in } = req.body;
  96. if (!address && !target) {
  97. throw new CustomError("Should at least update one field.");
  98. }
  99. const link = await query.link.find({
  100. uuid: req.params.id,
  101. ...(!req.user.admin && { user_id: req.user.id })
  102. });
  103. if (!link) {
  104. throw new CustomError("Link was not found.");
  105. }
  106. const targetDomain = utils.removeWww(URL.parse(target).hostname);
  107. const domain_id = link.domain_id || null;
  108. const queries = await Promise.all([
  109. validators.cooldown(req.user),
  110. validators.malware(req.user, target),
  111. address !== link.address &&
  112. query.link.find({
  113. address,
  114. domain_id
  115. }),
  116. validators.bannedDomain(targetDomain),
  117. validators.bannedHost(targetDomain)
  118. ]);
  119. // Check if custom link already exists
  120. if (queries[2]) {
  121. throw new CustomError("Custom URL is already in use.");
  122. }
  123. // Update link
  124. const [updatedLink] = await query.link.update(
  125. {
  126. id: link.id
  127. },
  128. {
  129. ...(address && { address }),
  130. ...(description && { description }),
  131. ...(target && { target }),
  132. ...(expire_in && { expire_in })
  133. }
  134. );
  135. return res.status(200).send(utils.sanitize.link({ ...link, ...updatedLink }));
  136. };
  137. export const remove: Handler = async (req, res) => {
  138. const link = await query.link.remove({
  139. uuid: req.params.id,
  140. ...(!req.user.admin && { user_id: req.user.id })
  141. });
  142. if (!link) {
  143. throw new CustomError("Could not delete the link");
  144. }
  145. return res
  146. .status(200)
  147. .send({ message: "Link has been deleted successfully." });
  148. };
  149. export const report: Handler = async (req, res) => {
  150. const { link } = req.body;
  151. const mail = await transporter.sendMail({
  152. from: env.MAIL_FROM || env.MAIL_USER,
  153. to: env.REPORT_EMAIL,
  154. subject: "[REPORT]",
  155. text: link,
  156. html: link
  157. });
  158. if (!mail.accepted.length) {
  159. throw new CustomError("Couldn't submit the report. Try again later.");
  160. }
  161. return res
  162. .status(200)
  163. .send({ message: "Thanks for the report, we'll take actions shortly." });
  164. };
  165. export const ban: Handler = async (req, res) => {
  166. const { id } = req.params;
  167. const update = {
  168. banned_by_id: req.user.id,
  169. banned: true
  170. };
  171. // 1. Check if link exists
  172. const link = await query.link.find({ uuid: id });
  173. if (!link) {
  174. throw new CustomError("No link has been found.", 400);
  175. }
  176. if (link.banned) {
  177. return res.status(200).send({ message: "Link has been banned already." });
  178. }
  179. const tasks = [];
  180. // 2. Ban link
  181. tasks.push(query.link.update({ uuid: id }, update));
  182. const domain = utils.removeWww(URL.parse(link.target).hostname);
  183. // 3. Ban target's domain
  184. if (req.body.domain) {
  185. tasks.push(query.domain.add({ ...update, address: domain }));
  186. }
  187. // 4. Ban target's host
  188. if (req.body.host) {
  189. const dnsRes = await dnsLookup(domain).catch(() => {
  190. throw new CustomError("Couldn't fetch DNS info.");
  191. });
  192. const host = dnsRes?.address;
  193. tasks.push(query.host.add({ ...update, address: host }));
  194. }
  195. // 5. Ban link owner
  196. if (req.body.user && link.user_id) {
  197. tasks.push(query.user.update({ id: link.user_id }, update));
  198. }
  199. // 6. Ban all of owner's links
  200. if (req.body.userLinks && link.user_id) {
  201. tasks.push(query.link.update({ user_id: link.user_id }, update));
  202. }
  203. // 7. Wait for all tasks to finish
  204. await Promise.all(tasks).catch(() => {
  205. throw new CustomError("Couldn't ban entries.");
  206. });
  207. // 8. Send response
  208. return res.status(200).send({ message: "Banned link successfully." });
  209. };
  210. export const redirect = (app: ReturnType<typeof next>): Handler => async (
  211. req,
  212. res,
  213. next
  214. ) => {
  215. const isBot = isbot(req.headers["user-agent"]);
  216. const isPreservedUrl = validators.preservedUrls.some(
  217. item => item === req.path.replace("/", "")
  218. );
  219. if (isPreservedUrl) return next();
  220. // 1. If custom domain, get domain info
  221. const host = utils.removeWww(req.headers.host);
  222. const domain =
  223. host !== env.DEFAULT_DOMAIN
  224. ? await query.domain.find({ address: host })
  225. : null;
  226. // 2. Get link
  227. const address = req.params.id.replace("+", "");
  228. const link = await query.link.find({
  229. address,
  230. domain_id: domain ? domain.id : null
  231. });
  232. // 3. When no link, if has domain redirect to domain's homepage
  233. // otherwise rediredt to 404
  234. if (!link) {
  235. return res.redirect(302, domain ? domain.homepage : "/404");
  236. }
  237. // 4. If link is banned, redirect to banned page.
  238. if (link.banned) {
  239. return res.redirect("/banned");
  240. }
  241. // 5. If wants to see link info, then redirect
  242. const doesRequestInfo = /.*\+$/gi.test(req.params.id);
  243. if (doesRequestInfo && !link.password) {
  244. return app.render(req, res, "/url-info", { target: link.target });
  245. }
  246. // 6. If link is protected, redirect to password page
  247. if (link.password) {
  248. return res.redirect(`/protected/${link.uuid}`);
  249. }
  250. // 7. Create link visit
  251. if (link.user_id && !isBot) {
  252. queue.visit.add({
  253. headers: req.headers,
  254. realIP: req.realIP,
  255. referrer: req.get("Referrer"),
  256. link
  257. });
  258. }
  259. // 8. Create Google Analytics visit
  260. if (env.GOOGLE_ANALYTICS_UNIVERSAL && !isBot) {
  261. ua(env.GOOGLE_ANALYTICS_UNIVERSAL)
  262. .pageview({
  263. dp: `/${address}`,
  264. ua: req.headers["user-agent"],
  265. uip: req.realIP,
  266. aip: 1
  267. })
  268. .send();
  269. }
  270. // 10. Redirect to target
  271. return res.redirect(link.target);
  272. };
  273. export const redirectProtected: Handler = async (req, res) => {
  274. // 1. Get link
  275. const uuid = req.params.id;
  276. const link = await query.link.find({ uuid });
  277. // 2. Throw error if no link
  278. if (!link || !link.password) {
  279. throw new CustomError("Couldn't find the link.", 400);
  280. }
  281. // 3. Check if password matches
  282. const matches = await bcrypt.compare(req.body.password, link.password);
  283. if (!matches) {
  284. throw new CustomError("Password is not correct.", 401);
  285. }
  286. // 4. Create visit
  287. if (link.user_id) {
  288. queue.visit.add({
  289. headers: req.headers,
  290. realIP: req.realIP,
  291. referrer: req.get("Referrer"),
  292. link
  293. });
  294. }
  295. // 5. Create Google Analytics visit
  296. if (env.GOOGLE_ANALYTICS_UNIVERSAL) {
  297. ua(env.GOOGLE_ANALYTICS_UNIVERSAL)
  298. .pageview({
  299. dp: `/${link.address}`,
  300. ua: req.headers["user-agent"],
  301. uip: req.realIP,
  302. aip: 1
  303. })
  304. .send();
  305. }
  306. // 6. Send target
  307. return res.status(200).send({ target: link.target });
  308. };
  309. export const redirectCustomDomain: Handler = async (req, res, next) => {
  310. const { path } = req;
  311. const host = utils.removeWww(req.headers.host);
  312. if (host === env.DEFAULT_DOMAIN) {
  313. return next();
  314. }
  315. if (
  316. path === "/" ||
  317. validators.preservedUrls
  318. .filter(l => l !== "url-password")
  319. .some(item => item === path.replace("/", ""))
  320. ) {
  321. const domain = await query.domain.find({ address: host });
  322. const redirectURL = domain
  323. ? domain.homepage
  324. : `https://${env.DEFAULT_DOMAIN + path}`;
  325. return res.redirect(302, redirectURL);
  326. }
  327. return next();
  328. };
  329. export const stats: Handler = async (req, res) => {
  330. const { user } = req;
  331. const uuid = req.params.id;
  332. const link = await query.link.find({
  333. ...(!user.admin && { user_id: user.id }),
  334. uuid
  335. });
  336. if (!link) {
  337. throw new CustomError("Link could not be found.");
  338. }
  339. const stats = await query.visit.find({ link_id: link.id }, link.visit_count);
  340. if (!stats) {
  341. throw new CustomError("Could not get the short link stats.");
  342. }
  343. return res.status(200).send({
  344. ...stats,
  345. ...utils.sanitize.link(link)
  346. });
  347. };