linkController.ts 12 KB

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