linkController.ts 11 KB

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