linkController.ts 12 KB

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