linkController.ts 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405
  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 {
  24. addProtocol,
  25. generateShortLink,
  26. getStatsCacheTime,
  27. getStatsLimit
  28. } from "../utils";
  29. import {
  30. checkBannedDomain,
  31. checkBannedHost,
  32. cooldownCheck,
  33. malwareCheck,
  34. preservedUrls,
  35. urlCountsCheck
  36. } from "./validateBodyController";
  37. import { visitQueue } from "../queues";
  38. const dnsLookup = promisify(dns.lookup);
  39. const generateId = async () => {
  40. const address = generate(
  41. "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890",
  42. 6
  43. );
  44. const link = await findLink({ address });
  45. if (!link) return address;
  46. return generateId();
  47. };
  48. export const shortener: Handler = async (req, res) => {
  49. try {
  50. const target = addProtocol(req.body.target);
  51. const targetDomain = URL.parse(target).hostname;
  52. const queries = await Promise.all([
  53. process.env.GOOGLE_SAFE_BROWSING_KEY && cooldownCheck(req.user),
  54. process.env.GOOGLE_SAFE_BROWSING_KEY &&
  55. 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 && Number(process.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 } = req.headers;
  112. const reqestedId = req.params.id || req.body.id;
  113. const address = reqestedId.replace("+", "");
  114. const customDomain = host !== process.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 !== process.env.DEFAULT_DOMAIN) {
  123. if (!domain || !domain.homepage) return next();
  124. return res.redirect(301, domain.homepage);
  125. }
  126. return next();
  127. }
  128. if (link.banned) {
  129. return res.redirect("/banned");
  130. }
  131. const doesRequestInfo = /.*\+$/gi.test(reqestedId);
  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 && link.visit_count < getStatsLimit()) {
  148. visitQueue.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 && link.visit_count < getStatsLimit()) {
  159. visitQueue.add({
  160. headers: req.headers,
  161. realIP: req.realIP,
  162. referrer: req.get("Referrer"),
  163. link,
  164. customDomain
  165. });
  166. }
  167. if (process.env.GOOGLE_ANALYTICS_UNIVERSAL && !isBot) {
  168. const visitor = ua(process.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 = 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 === process.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 { headers, path } = req;
  244. if (
  245. headers.host !== process.env.DEFAULT_DOMAIN &&
  246. (path === "/" ||
  247. preservedUrls
  248. .filter(l => l !== "url-password")
  249. .some(item => item === path.replace("/", "")))
  250. ) {
  251. const domain = await getDomain({ address: headers.host });
  252. return res.redirect(
  253. 301,
  254. (domain && domain.homepage) ||
  255. `https://${process.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 !== process.env.DEFAULT_DOMAIN && 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 } = URL.parse(req.query.domain);
  280. const hasCustomDomain =
  281. req.query.domain && hostname !== process.env.DEFAULT_DOMAIN;
  282. const customDomain = hasCustomDomain
  283. ? (await getDomain({ address: req.query.domain })) || ({ id: -1 } as Domain)
  284. : ({} as Domain);
  285. const redisKey = req.query.id + (customDomain.address || "") + req.user.email;
  286. const cached = await redis.get(redisKey);
  287. if (cached) return res.status(200).json(JSON.parse(cached));
  288. const link = await findLink({
  289. address: req.query.id,
  290. domain_id: hasCustomDomain ? customDomain.id : null,
  291. user_id: req.user && req.user.id
  292. });
  293. if (!link) {
  294. return res.status(400).json({ error: "Couldn't find the short link." });
  295. }
  296. const stats = await getStats(link, customDomain);
  297. if (!stats) {
  298. return res
  299. .status(400)
  300. .json({ error: "Could not get the short link stats." });
  301. }
  302. const cacheTime = getStatsCacheTime(0);
  303. redis.set(redisKey, JSON.stringify(stats), "EX", cacheTime);
  304. return res.status(200).json(stats);
  305. };
  306. export const reportLink: Handler = async (req, res) => {
  307. if (!req.body.link) {
  308. return res.status(400).json({ error: "No URL has been provided." });
  309. }
  310. const { hostname } = URL.parse(req.body.link);
  311. if (hostname !== process.env.DEFAULT_DOMAIN) {
  312. return res.status(400).json({
  313. error: `You can only report a ${process.env.DEFAULT_DOMAIN} link`
  314. });
  315. }
  316. const mail = await transporter.sendMail({
  317. from: process.env.MAIL_USER,
  318. to: process.env.REPORT_MAIL,
  319. subject: "[REPORT]",
  320. text: req.body.link,
  321. html: req.body.link
  322. });
  323. if (mail.accepted.length) {
  324. return res
  325. .status(200)
  326. .json({ message: "Thanks for the report, we'll take actions shortly." });
  327. }
  328. return res
  329. .status(400)
  330. .json({ error: "Couldn't submit the report. Try again later." });
  331. };
  332. export const ban: Handler = async (req, res) => {
  333. if (!req.body.id)
  334. return res.status(400).json({ error: "No id has been provided." });
  335. const link = await findLink({ address: req.body.id, domain_id: null });
  336. if (!link) return res.status(400).json({ error: "Couldn't find the link." });
  337. if (link.banned) {
  338. return res.status(200).json({ message: "Link was banned already." });
  339. }
  340. const domain = URL.parse(link.target).hostname;
  341. let host;
  342. if (req.body.host) {
  343. try {
  344. const dnsRes = await dnsLookup(domain);
  345. host = dnsRes && dnsRes.address;
  346. } catch (error) {
  347. host = null;
  348. }
  349. }
  350. await banLink({
  351. adminId: req.user.id,
  352. domain,
  353. host,
  354. address: req.body.id,
  355. banUser: !!req.body.user
  356. });
  357. return res.status(200).json({ message: "Link has been banned successfully" });
  358. };