linkController.ts 12 KB

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