urlController.js 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362
  1. const { promisify } = require('util');
  2. const urlRegex = require('url-regex');
  3. const dns = require('dns');
  4. const URL = require('url');
  5. const generate = require('nanoid/generate');
  6. const useragent = require('useragent');
  7. const geoip = require('geoip-lite');
  8. const bcrypt = require('bcryptjs');
  9. const ua = require('universal-analytics');
  10. const isbot = require('isbot');
  11. const { addIPCooldown } = require('../db/user');
  12. const {
  13. addUrlCount,
  14. createShortUrl,
  15. createVisit,
  16. deleteCustomDomain,
  17. deleteUrl,
  18. findUrl,
  19. getCountUrls,
  20. getCustomDomain,
  21. getStats,
  22. getUrls,
  23. setCustomDomain,
  24. banUrl,
  25. } = require('../db/url');
  26. const {
  27. checkBannedDomain,
  28. checkBannedHost,
  29. cooldownCheck,
  30. malwareCheck,
  31. preservedUrls,
  32. urlCountsCheck,
  33. } = require('./validateBodyController');
  34. const transporter = require('../mail/mail');
  35. const redis = require('../redis');
  36. const { addProtocol, getStatsLimit, generateShortUrl, getStatsCacheTime } = require('../utils');
  37. const dnsLookup = promisify(dns.lookup);
  38. const generateId = async () => {
  39. const id = generate('abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890', 6);
  40. const urls = await findUrl({ id });
  41. if (!urls.length) return id;
  42. return generateId();
  43. };
  44. exports.urlShortener = async ({ body, realIp, user }, res) => {
  45. try {
  46. const domain = URL.parse(body.target).hostname;
  47. const queries = await Promise.all([
  48. process.env.GOOGLE_SAFE_BROWSING_KEY && cooldownCheck(user),
  49. process.env.GOOGLE_SAFE_BROWSING_KEY && malwareCheck(user, body.target),
  50. user && urlCountsCheck(user.email),
  51. user && body.reuse && findUrl({ target: addProtocol(body.target) }),
  52. user && body.customurl && findUrl({ id: body.customurl || '' }),
  53. (!user || !body.customurl) && generateId(),
  54. checkBannedDomain(domain),
  55. checkBannedHost(domain),
  56. ]);
  57. // if "reuse" is true, try to return
  58. // the existent URL without creating one
  59. if (user && body.reuse) {
  60. const urls = queries[3];
  61. if (urls.length) {
  62. urls.sort((a, b) => a.createdAt > b.createdAt);
  63. const { domain: d, user: u, ...url } = urls[urls.length - 1];
  64. const data = {
  65. ...url,
  66. password: !!url.password,
  67. reuse: true,
  68. shortUrl: generateShortUrl(url.id, user.domain, user.useHttps),
  69. };
  70. return res.json(data);
  71. }
  72. }
  73. // Check if custom URL already exists
  74. if (user && body.customurl) {
  75. const urls = queries[4];
  76. if (urls.length) {
  77. const urlWithNoDomain = !user.domain && urls.some(url => !url.domain);
  78. const urlWithDmoain = user.domain && urls.some(url => url.domain === user.domain);
  79. if (urlWithNoDomain || urlWithDmoain) {
  80. throw new Error('Custom URL is already in use.');
  81. }
  82. }
  83. }
  84. // Create new URL
  85. const id = (user && body.customurl) || queries[5];
  86. const target = addProtocol(body.target);
  87. const url = await createShortUrl({ ...body, id, target, user });
  88. if (!user && Number(process.env.NON_USER_COOLDOWN)) {
  89. addIPCooldown(realIp);
  90. }
  91. return res.json(url);
  92. } catch (error) {
  93. return res.status(400).json({ error: error.message });
  94. }
  95. };
  96. const browsersList = ['IE', 'Firefox', 'Chrome', 'Opera', 'Safari', 'Edge'];
  97. const osList = ['Windows', 'Mac Os X', 'Linux', 'Chrome OS', 'Android', 'iOS'];
  98. const filterInBrowser = agent => item =>
  99. agent.family.toLowerCase().includes(item.toLocaleLowerCase());
  100. const filterInOs = agent => item =>
  101. agent.os.family.toLowerCase().includes(item.toLocaleLowerCase());
  102. exports.goToUrl = async (req, res, next) => {
  103. const { host } = req.headers;
  104. const reqestedId = req.params.id || req.body.id;
  105. const id = reqestedId.replace('+', '');
  106. const domain = host !== process.env.DEFAULT_DOMAIN && host;
  107. const agent = useragent.parse(req.headers['user-agent']);
  108. const [browser = 'Other'] = browsersList.filter(filterInBrowser(agent));
  109. const [os = 'Other'] = osList.filter(filterInOs(agent));
  110. const referrer = req.header('Referer') && URL.parse(req.header('Referer')).hostname;
  111. const location = geoip.lookup(req.realIp);
  112. const country = location && location.country;
  113. const isBot = isbot(req.headers['user-agent']);
  114. let url;
  115. const cachedUrl = await redis.get(id + (domain || ''));
  116. if (cachedUrl) {
  117. url = JSON.parse(cachedUrl);
  118. } else {
  119. const urls = await findUrl({ id, domain });
  120. url =
  121. urls && urls.length && urls.find(item => (domain ? item.domain === domain : !item.domain));
  122. }
  123. if (!url) {
  124. if (host !== process.env.DEFAULT_DOMAIN) {
  125. const { homepage } = await getCustomDomain({ customDomain: domain });
  126. if (!homepage) return next();
  127. return res.redirect(301, homepage);
  128. }
  129. return next();
  130. }
  131. redis.set(id + (domain || ''), JSON.stringify(url), 'EX', 60 * 60 * 1);
  132. if (url.banned) {
  133. return res.redirect('/banned');
  134. }
  135. const doesRequestInfo = /.*\+$/gi.test(reqestedId);
  136. if (doesRequestInfo && !url.password) {
  137. req.urlTarget = url.target;
  138. req.pageType = 'info';
  139. return next();
  140. }
  141. if (url.password && !req.body.password) {
  142. req.protectedUrl = id;
  143. req.pageType = 'password';
  144. return next();
  145. }
  146. if (url.password) {
  147. const isMatch = await bcrypt.compare(req.body.password, url.password);
  148. if (!isMatch) {
  149. return res.status(401).json({ error: 'Password is not correct' });
  150. }
  151. if (url.user && !isBot) {
  152. addUrlCount(url.id, domain);
  153. createVisit({
  154. browser,
  155. country: country || 'Unknown',
  156. domain,
  157. id: url.id,
  158. os,
  159. referrer: referrer || 'Direct',
  160. limit: getStatsLimit(url),
  161. });
  162. }
  163. return res.status(200).json({ target: url.target });
  164. }
  165. if (url.user && !isBot) {
  166. addUrlCount(url.id, domain);
  167. createVisit({
  168. browser,
  169. country: country || 'Unknown',
  170. domain,
  171. id: url.id,
  172. os,
  173. referrer: referrer || 'Direct',
  174. limit: getStatsLimit(url),
  175. });
  176. }
  177. if (process.env.GOOGLE_ANALYTICS_UNIVERSAL && !isBot) {
  178. const visitor = ua(process.env.GOOGLE_ANALYTICS_UNIVERSAL);
  179. visitor
  180. .pageview({
  181. dp: `/${id}`,
  182. ua: req.headers['user-agent'],
  183. uip: req.realIp,
  184. aip: 1,
  185. })
  186. .send();
  187. }
  188. return res.redirect(url.target);
  189. };
  190. exports.getUrls = async ({ query, user }, res) => {
  191. const { countAll } = await getCountUrls({ user });
  192. const urlsList = await getUrls({ options: query, user });
  193. const isCountMissing = urlsList.list.some(url => typeof url.count === 'undefined');
  194. const { list } = isCountMissing
  195. ? await getUrls({ options: query, user, setCount: true })
  196. : urlsList;
  197. return res.json({ list, countAll });
  198. };
  199. exports.setCustomDomain = async ({ body, user }, res) => {
  200. const parsed = URL.parse(body.customDomain);
  201. const customDomain = parsed.hostname || parsed.href;
  202. if (!customDomain) return res.status(400).json({ error: 'Domain is not valid.' });
  203. if (customDomain.length > 40) {
  204. return res.status(400).json({ error: 'Maximum custom domain length is 40.' });
  205. }
  206. if (customDomain === process.env.DEFAULT_DOMAIN) {
  207. return res.status(400).json({ error: "You can't use default domain." });
  208. }
  209. const isValidHomepage =
  210. !body.homepage || urlRegex({ exact: true, strict: false }).test(body.homepage);
  211. if (!isValidHomepage) return res.status(400).json({ error: 'Homepage is not valid.' });
  212. const homepage =
  213. body.homepage &&
  214. (URL.parse(body.homepage).protocol ? body.homepage : `http://${body.homepage}`);
  215. const { email } = await getCustomDomain({ customDomain });
  216. if (email && email !== user.email) {
  217. return res
  218. .status(400)
  219. .json({ error: 'Domain is already taken. Contact us for multiple users.' });
  220. }
  221. const userCustomDomain = await setCustomDomain({
  222. user,
  223. customDomain,
  224. homepage,
  225. useHttps: body.useHttps,
  226. });
  227. if (userCustomDomain)
  228. return res.status(201).json({
  229. customDomain: userCustomDomain.name,
  230. homepage: userCustomDomain.homepage,
  231. useHttps: userCustomDomain.useHttps,
  232. });
  233. return res.status(400).json({ error: "Couldn't set custom domain." });
  234. };
  235. exports.deleteCustomDomain = async ({ user }, res) => {
  236. const response = await deleteCustomDomain({ user });
  237. if (response) return res.status(200).json({ message: 'Domain deleted successfully' });
  238. return res.status(400).json({ error: "Couldn't delete custom domain." });
  239. };
  240. exports.customDomainRedirection = async (req, res, next) => {
  241. const { headers, path } = req;
  242. if (
  243. headers.host !== process.env.DEFAULT_DOMAIN &&
  244. (path === '/' ||
  245. preservedUrls.filter(u => u !== 'url-password').some(item => item === path.replace('/', '')))
  246. ) {
  247. const { homepage } = await getCustomDomain({ customDomain: headers.host });
  248. return res.redirect(301, homepage || `https://${process.env.DEFAULT_DOMAIN + path}`);
  249. }
  250. return next();
  251. };
  252. exports.deleteUrl = async ({ body: { id, domain }, user }, res) => {
  253. if (!id) return res.status(400).json({ error: 'No id has been provided.' });
  254. const customDomain = domain !== process.env.DEFAULT_DOMAIN && domain;
  255. const urls = await findUrl({ id, domain: customDomain });
  256. if (!urls && !urls.length) return res.status(400).json({ error: "Couldn't find the short URL." });
  257. redis.del(id + (customDomain || ''));
  258. const response = await deleteUrl({ id, domain: customDomain, user });
  259. if (response) return res.status(200).json({ message: 'Short URL deleted successfully' });
  260. return res.status(400).json({ error: "Couldn't delete short URL." });
  261. };
  262. exports.getStats = async ({ query: { id, domain }, user }, res) => {
  263. if (!id) return res.status(400).json({ error: 'No id has been provided.' });
  264. const customDomain = domain !== process.env.DEFAULT_DOMAIN && domain;
  265. const redisKey = id + (customDomain || '') + user.email;
  266. const cached = await redis.get(redisKey);
  267. if (cached) return res.status(200).json(JSON.parse(cached));
  268. const urls = await findUrl({ id, domain: customDomain });
  269. if (!urls && !urls.length) return res.status(400).json({ error: "Couldn't find the short URL." });
  270. const [url] = urls;
  271. const stats = await getStats({ id, domain: customDomain, user });
  272. if (!stats) return res.status(400).json({ error: 'Could not get the short URL stats.' });
  273. stats.total = url.count && url.count.toNumber ? url.count.toNumber() : 0;
  274. stats.shortUrl = `http${!domain ? 's' : ''}://${
  275. domain ? url.domain : process.env.DEFAULT_DOMAIN
  276. }/${url.id}`;
  277. stats.target = url.target;
  278. const cacheTime = getStatsCacheTime(stats.total);
  279. redis.set(redisKey, JSON.stringify(stats), 'EX', cacheTime);
  280. return res.status(200).json(stats);
  281. };
  282. exports.reportUrl = async ({ body: { url } }, res) => {
  283. if (!url) return res.status(400).json({ error: 'No URL has been provided.' });
  284. const isValidUrl = urlRegex({ exact: true, strict: false }).test(url);
  285. if (!isValidUrl) return res.status(400).json({ error: 'URL is not valid.' });
  286. const mail = await transporter.sendMail({
  287. from: process.env.MAIL_FROM || process.env.MAIL_USER,
  288. to: process.env.REPORT_MAIL,
  289. subject: '[REPORT]',
  290. text: url,
  291. html: url,
  292. });
  293. if (mail.accepted.length) {
  294. return res.status(200).json({ message: "Thanks for the report, we'll take actions shortly." });
  295. }
  296. return res.status(400).json({ error: "Couldn't submit the report. Try again later." });
  297. };
  298. exports.ban = async ({ body, user }, res) => {
  299. if (!body.id) return res.status(400).json({ error: 'No id has been provided.' });
  300. const urls = await findUrl({ id: body.id });
  301. const [url] = urls.filter(item => !item.domain);
  302. if (!url) return res.status(400).json({ error: "Couldn't find the URL." });
  303. if (url.banned) return res.status(200).json({ message: 'URL was banned already' });
  304. redis.del(body.id);
  305. const domain = URL.parse(url.target).hostname;
  306. let host;
  307. if (body.host) {
  308. try {
  309. const dnsRes = await dnsLookup(domain);
  310. host = dnsRes && dnsRes.address;
  311. } catch (error) {
  312. host = null;
  313. }
  314. }
  315. await banUrl({
  316. adminEmail: user.email,
  317. domain: body.domain && domain,
  318. host,
  319. id: body.id,
  320. user: body.user,
  321. });
  322. return res.status(200).json({ message: 'URL has been banned successfully' });
  323. };