linkController.ts 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398
  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 env from "../../env";
  13. import {
  14. banLink,
  15. createShortLink,
  16. deleteLink,
  17. findLink,
  18. getLinks,
  19. getStats,
  20. getUserLinksCount
  21. } from "../db/link";
  22. import transporter from "../../mail/mail";
  23. import * as redis from "../../redis";
  24. import { addProtocol, generateShortLink, getStatsCacheTime } from "../../utils";
  25. import {
  26. checkBannedDomain,
  27. checkBannedHost,
  28. cooldownCheck,
  29. malwareCheck,
  30. preservedUrls,
  31. urlCountsCheck
  32. } from "./validateBodyController";
  33. import queue from "../../queues";
  34. const dnsLookup = promisify(dns.lookup);
  35. const generateId = async () => {
  36. const address = generate(
  37. "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890",
  38. env.LINK_LENGTH
  39. );
  40. const link = await findLink({ address });
  41. if (!link) return address;
  42. return generateId();
  43. };
  44. export const shortener: Handler = async (req, res) => {
  45. try {
  46. const target = addProtocol(req.body.target);
  47. const targetDomain = URL.parse(target).hostname;
  48. const queries = await Promise.all([
  49. env.GOOGLE_SAFE_BROWSING_KEY && cooldownCheck(req.user),
  50. env.GOOGLE_SAFE_BROWSING_KEY && 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 && 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 !== 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 !== 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. queue.visit.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. queue.visit.add({
  155. headers: req.headers,
  156. realIP: req.realIP,
  157. referrer: req.get("Referrer"),
  158. link,
  159. customDomain
  160. });
  161. }
  162. if (env.GOOGLE_ANALYTICS_UNIVERSAL && !isBot) {
  163. const visitor = ua(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 === 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 !== 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) || `https://${env.DEFAULT_DOMAIN + path}`
  250. );
  251. }
  252. return next();
  253. };
  254. export const deleteUserLink: Handler = async (req, res) => {
  255. const { id, domain } = req.body;
  256. if (!id) {
  257. return res.status(400).json({ error: "No id has been provided." });
  258. }
  259. const response = await deleteLink({
  260. address: id,
  261. domain: !domain || domain === env.DEFAULT_DOMAIN ? null : domain,
  262. user_id: req.user.id
  263. });
  264. if (response) {
  265. return res.status(200).json({ message: "Short link deleted successfully" });
  266. }
  267. return res.status(400).json({ error: "Couldn't delete the short link." });
  268. };
  269. export const getLinkStats: Handler = async (req, res) => {
  270. if (!req.query.id) {
  271. return res.status(400).json({ error: "No id has been provided." });
  272. }
  273. const { hostname } = URL.parse(req.query.domain);
  274. const hasCustomDomain = req.query.domain && hostname !== env.DEFAULT_DOMAIN;
  275. const customDomain = hasCustomDomain
  276. ? (await getDomain({ address: req.query.domain })) || ({ id: -1 } as Domain)
  277. : ({} as Domain);
  278. const redisKey = req.query.id + (customDomain.address || "") + req.user.email;
  279. const cached = await redis.get(redisKey);
  280. if (cached) return res.status(200).json(JSON.parse(cached));
  281. const link = await findLink({
  282. address: req.query.id,
  283. domain_id: hasCustomDomain ? customDomain.id : null,
  284. user_id: req.user && req.user.id
  285. });
  286. if (!link) {
  287. return res.status(400).json({ error: "Couldn't find the short link." });
  288. }
  289. const stats = await getStats(link, customDomain);
  290. if (!stats) {
  291. return res
  292. .status(400)
  293. .json({ error: "Could not get the short link stats." });
  294. }
  295. const cacheTime = getStatsCacheTime(0);
  296. redis.set(redisKey, JSON.stringify(stats), "EX", cacheTime);
  297. return res.status(200).json(stats);
  298. };
  299. export const reportLink: Handler = async (req, res) => {
  300. if (!req.body.link) {
  301. return res.status(400).json({ error: "No URL has been provided." });
  302. }
  303. const { hostname } = URL.parse(req.body.link);
  304. if (hostname !== env.DEFAULT_DOMAIN) {
  305. return res.status(400).json({
  306. error: `You can only report a ${env.DEFAULT_DOMAIN} link`
  307. });
  308. }
  309. const mail = await transporter.sendMail({
  310. from: env.MAIL_USER,
  311. to: env.REPORT_MAIL,
  312. subject: "[REPORT]",
  313. text: req.body.link,
  314. html: req.body.link
  315. });
  316. if (mail.accepted.length) {
  317. return res
  318. .status(200)
  319. .json({ message: "Thanks for the report, we'll take actions shortly." });
  320. }
  321. return res
  322. .status(400)
  323. .json({ error: "Couldn't submit the report. Try again later." });
  324. };
  325. export const ban: Handler = async (req, res) => {
  326. if (!req.body.id)
  327. return res.status(400).json({ error: "No id has been provided." });
  328. const link = await findLink({ address: req.body.id, domain_id: null });
  329. if (!link) return res.status(400).json({ error: "Link does not exist." });
  330. if (link.banned) {
  331. return res.status(200).json({ message: "Link was banned already." });
  332. }
  333. const domain = URL.parse(link.target).hostname;
  334. let host;
  335. if (req.body.host) {
  336. try {
  337. const dnsRes = await dnsLookup(domain);
  338. host = dnsRes && dnsRes.address;
  339. } catch (error) {
  340. host = null;
  341. }
  342. }
  343. await banLink({
  344. adminId: req.user.id,
  345. domain,
  346. host,
  347. address: req.body.id,
  348. banUser: !!req.body.user
  349. });
  350. return res.status(200).json({ message: "Link has been banned successfully" });
  351. };