linkController.ts 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400
  1. import bcrypt from "bcryptjs";
  2. import dns from "dns";
  3. import { Handler } from "express";
  4. import isbot from "isbot";
  5. import generate from "nanoid/generate";
  6. import ua from "universal-analytics";
  7. import URL from "url";
  8. import urlRegex from "url-regex";
  9. import { promisify } from "util";
  10. import { deleteDomain, getDomain, setDomain } from "../db/domain";
  11. import { addIP } from "../db/ip";
  12. import {
  13. banLink,
  14. createShortLink,
  15. deleteLink,
  16. findLink,
  17. getLinks,
  18. getStats,
  19. getUserLinksCount
  20. } from "../db/link";
  21. import transporter from "../mail/mail";
  22. import * as redis from "../redis";
  23. import { addProtocol, generateShortLink, getStatsCacheTime } from "../utils";
  24. import {
  25. checkBannedDomain,
  26. checkBannedHost,
  27. cooldownCheck,
  28. malwareCheck,
  29. preservedUrls,
  30. urlCountsCheck
  31. } from "./validateBodyController";
  32. import { visitQueue } from "../queues";
  33. const dnsLookup = promisify(dns.lookup);
  34. const generateId = async () => {
  35. const address = generate(
  36. "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890",
  37. Number(process.env.LINK_LENGTH) || 6
  38. );
  39. const link = await findLink({ address });
  40. if (!link) return address;
  41. return generateId();
  42. };
  43. export const shortener: Handler = async (req, res) => {
  44. try {
  45. const target = addProtocol(req.body.target);
  46. const targetDomain = URL.parse(target).hostname;
  47. const queries = await Promise.all([
  48. process.env.GOOGLE_SAFE_BROWSING_KEY && cooldownCheck(req.user),
  49. process.env.GOOGLE_SAFE_BROWSING_KEY &&
  50. malwareCheck(req.user, req.body.target),
  51. req.user && urlCountsCheck(req.user),
  52. req.user &&
  53. req.body.reuse &&
  54. findLink({
  55. target,
  56. user_id: req.user.id
  57. }),
  58. req.user &&
  59. req.body.customurl &&
  60. findLink({
  61. address: req.body.customurl,
  62. domain_id: req.user.domain_id || null
  63. }),
  64. (!req.user || !req.body.customurl) && generateId(),
  65. checkBannedDomain(targetDomain),
  66. checkBannedHost(targetDomain)
  67. ]);
  68. // if "reuse" is true, try to return
  69. // the existent URL without creating one
  70. if (queries[3]) {
  71. const { domain_id: d, user_id: u, ...link } = queries[3];
  72. const shortLink = generateShortLink(link.address, req.user.domain);
  73. const data = {
  74. ...link,
  75. id: link.address,
  76. password: !!link.password,
  77. reuse: true,
  78. shortLink,
  79. shortUrl: shortLink
  80. };
  81. return res.json(data);
  82. }
  83. // Check if custom link already exists
  84. if (queries[4]) {
  85. throw new Error("Custom URL is already in use.");
  86. }
  87. // Create new link
  88. const address = (req.user && req.body.customurl) || queries[5];
  89. const link = await createShortLink(
  90. {
  91. ...req.body,
  92. address,
  93. target
  94. },
  95. req.user
  96. );
  97. if (!req.user && Number(process.env.NON_USER_COOLDOWN)) {
  98. addIP(req.realIP);
  99. }
  100. return res.json({ ...link, id: link.address });
  101. } catch (error) {
  102. return res.status(400).json({ error: error.message });
  103. }
  104. };
  105. export const goToLink: Handler = async (req, res, next) => {
  106. const { host } = req.headers;
  107. const reqestedId = req.params.id || req.body.id;
  108. const address = reqestedId.replace("+", "");
  109. const customDomain = host !== process.env.DEFAULT_DOMAIN && host;
  110. const isBot = isbot(req.headers["user-agent"]);
  111. let domain;
  112. if (customDomain) {
  113. domain = await getDomain({ address: customDomain });
  114. }
  115. const link = await findLink({ address, domain_id: domain && domain.id });
  116. if (!link) {
  117. if (host !== process.env.DEFAULT_DOMAIN) {
  118. if (!domain || !domain.homepage) return next();
  119. return res.redirect(301, domain.homepage);
  120. }
  121. return next();
  122. }
  123. if (link.banned) {
  124. return res.redirect("/banned");
  125. }
  126. const doesRequestInfo = /.*\+$/gi.test(reqestedId);
  127. if (doesRequestInfo && !link.password) {
  128. req.linkTarget = link.target;
  129. req.pageType = "info";
  130. return next();
  131. }
  132. if (link.password && !req.body.password) {
  133. req.protectedLink = address;
  134. req.pageType = "password";
  135. return next();
  136. }
  137. if (link.password) {
  138. const isMatch = await bcrypt.compare(req.body.password, link.password);
  139. if (!isMatch) {
  140. return res.status(401).json({ error: "Password is not correct" });
  141. }
  142. if (link.user_id && !isBot) {
  143. visitQueue.add({
  144. headers: req.headers,
  145. realIP: req.realIP,
  146. referrer: req.get("Referrer"),
  147. link,
  148. customDomain
  149. });
  150. }
  151. return res.status(200).json({ target: link.target });
  152. }
  153. if (link.user_id && !isBot) {
  154. visitQueue.add({
  155. headers: req.headers,
  156. realIP: req.realIP,
  157. referrer: req.get("Referrer"),
  158. link,
  159. customDomain
  160. });
  161. }
  162. if (process.env.GOOGLE_ANALYTICS_UNIVERSAL && !isBot) {
  163. const visitor = ua(process.env.GOOGLE_ANALYTICS_UNIVERSAL);
  164. visitor
  165. .pageview({
  166. dp: `/${address}`,
  167. ua: req.headers["user-agent"],
  168. uip: req.realIP,
  169. aip: 1
  170. })
  171. .send();
  172. }
  173. return res.redirect(link.target);
  174. };
  175. export const getUserLinks: Handler = async (req, res) => {
  176. const [countAll, list] = await Promise.all([
  177. getUserLinksCount({ user_id: req.user.id }),
  178. getLinks(req.user.id, req.query)
  179. ]);
  180. return res.json({ list, countAll: parseInt(countAll) });
  181. };
  182. export const setCustomDomain: Handler = async (req, res) => {
  183. const parsed = URL.parse(req.body.customDomain);
  184. const customDomain = parsed.hostname || parsed.href;
  185. if (!customDomain)
  186. return res.status(400).json({ error: "Domain is not valid." });
  187. if (customDomain.length > 40) {
  188. return res
  189. .status(400)
  190. .json({ error: "Maximum custom domain length is 40." });
  191. }
  192. if (customDomain === process.env.DEFAULT_DOMAIN) {
  193. return res.status(400).json({ error: "You can't use default domain." });
  194. }
  195. const isValidHomepage =
  196. !req.body.homepage ||
  197. urlRegex({ exact: true, strict: false }).test(req.body.homepage);
  198. if (!isValidHomepage)
  199. return res.status(400).json({ error: "Homepage is not valid." });
  200. const homepage =
  201. req.body.homepage &&
  202. (URL.parse(req.body.homepage).protocol
  203. ? req.body.homepage
  204. : `http://${req.body.homepage}`);
  205. const matchedDomain = await getDomain({ address: customDomain });
  206. if (
  207. matchedDomain &&
  208. matchedDomain.user_id &&
  209. matchedDomain.user_id !== req.user.id
  210. ) {
  211. return res.status(400).json({
  212. error: "Domain is already taken. Contact us for multiple users."
  213. });
  214. }
  215. const userCustomDomain = await setDomain(
  216. {
  217. address: customDomain,
  218. homepage
  219. },
  220. req.user,
  221. matchedDomain
  222. );
  223. if (userCustomDomain) {
  224. return res.status(201).json({
  225. customDomain: userCustomDomain.address,
  226. homepage: userCustomDomain.homepage
  227. });
  228. }
  229. return res.status(400).json({ error: "Couldn't set custom domain." });
  230. };
  231. export const deleteCustomDomain: Handler = async (req, res) => {
  232. const response = await deleteDomain(req.user);
  233. if (response)
  234. return res.status(200).json({ message: "Domain deleted successfully" });
  235. return res.status(400).json({ error: "Couldn't delete custom domain." });
  236. };
  237. export const customDomainRedirection: Handler = async (req, res, next) => {
  238. const { headers, path } = req;
  239. if (
  240. headers.host !== process.env.DEFAULT_DOMAIN &&
  241. (path === "/" ||
  242. preservedUrls
  243. .filter(l => l !== "url-password")
  244. .some(item => item === path.replace("/", "")))
  245. ) {
  246. const domain = await getDomain({ address: headers.host });
  247. return res.redirect(
  248. 301,
  249. (domain && domain.homepage) ||
  250. `https://${process.env.DEFAULT_DOMAIN + path}`
  251. );
  252. }
  253. return next();
  254. };
  255. export const deleteUserLink: Handler = async (req, res) => {
  256. const { id, domain } = req.body;
  257. if (!id) {
  258. return res.status(400).json({ error: "No id has been provided." });
  259. }
  260. const response = await deleteLink({
  261. address: id,
  262. domain: !domain || domain === process.env.DEFAULT_DOMAIN ? null : domain,
  263. user_id: req.user.id
  264. });
  265. if (response) {
  266. return res.status(200).json({ message: "Short link deleted successfully" });
  267. }
  268. return res.status(400).json({ error: "Couldn't delete the short link." });
  269. };
  270. export const getLinkStats: Handler = async (req, res) => {
  271. if (!req.query.id) {
  272. return res.status(400).json({ error: "No id has been provided." });
  273. }
  274. const { hostname } = URL.parse(req.query.domain);
  275. const hasCustomDomain =
  276. req.query.domain && hostname !== process.env.DEFAULT_DOMAIN;
  277. const customDomain = hasCustomDomain
  278. ? (await getDomain({ address: req.query.domain })) || ({ id: -1 } as Domain)
  279. : ({} as Domain);
  280. const redisKey = req.query.id + (customDomain.address || "") + req.user.email;
  281. const cached = await redis.get(redisKey);
  282. if (cached) return res.status(200).json(JSON.parse(cached));
  283. const link = await findLink({
  284. address: req.query.id,
  285. domain_id: hasCustomDomain ? customDomain.id : null,
  286. user_id: req.user && req.user.id
  287. });
  288. if (!link) {
  289. return res.status(400).json({ error: "Couldn't find the short link." });
  290. }
  291. const stats = await getStats(link, customDomain);
  292. if (!stats) {
  293. return res
  294. .status(400)
  295. .json({ error: "Could not get the short link stats." });
  296. }
  297. const cacheTime = getStatsCacheTime(0);
  298. redis.set(redisKey, JSON.stringify(stats), "EX", cacheTime);
  299. return res.status(200).json(stats);
  300. };
  301. export const reportLink: Handler = async (req, res) => {
  302. if (!req.body.link) {
  303. return res.status(400).json({ error: "No URL has been provided." });
  304. }
  305. const { hostname } = URL.parse(req.body.link);
  306. if (hostname !== process.env.DEFAULT_DOMAIN) {
  307. return res.status(400).json({
  308. error: `You can only report a ${process.env.DEFAULT_DOMAIN} link`
  309. });
  310. }
  311. const mail = await transporter.sendMail({
  312. from: process.env.MAIL_USER,
  313. to: process.env.REPORT_MAIL,
  314. subject: "[REPORT]",
  315. text: req.body.link,
  316. html: req.body.link
  317. });
  318. if (mail.accepted.length) {
  319. return res
  320. .status(200)
  321. .json({ message: "Thanks for the report, we'll take actions shortly." });
  322. }
  323. return res
  324. .status(400)
  325. .json({ error: "Couldn't submit the report. Try again later." });
  326. };
  327. export const ban: Handler = async (req, res) => {
  328. if (!req.body.id)
  329. return res.status(400).json({ error: "No id has been provided." });
  330. const link = await findLink({ address: req.body.id, domain_id: null });
  331. if (!link) return res.status(400).json({ error: "Couldn't find the link." });
  332. if (link.banned) {
  333. return res.status(200).json({ message: "Link was banned already." });
  334. }
  335. const domain = URL.parse(link.target).hostname;
  336. let host;
  337. if (req.body.host) {
  338. try {
  339. const dnsRes = await dnsLookup(domain);
  340. host = dnsRes && dnsRes.address;
  341. } catch (error) {
  342. host = null;
  343. }
  344. }
  345. await banLink({
  346. adminId: req.user.id,
  347. domain,
  348. host,
  349. address: req.body.id,
  350. banUser: !!req.body.user
  351. });
  352. return res.status(200).json({ message: "Link has been banned successfully" });
  353. };