urlController.js 7.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197
  1. const urlRegex = require('url-regex');
  2. const URL = require('url');
  3. const useragent = require('useragent');
  4. const geoip = require('geoip-lite');
  5. const bcrypt = require('bcryptjs');
  6. const axios = require('axios');
  7. const {
  8. createShortUrl,
  9. createVisit,
  10. findUrl,
  11. getStats,
  12. getUrls,
  13. getCustomDomain,
  14. setCustomDomain,
  15. deleteCustomDomain,
  16. deleteUrl,
  17. } = require('../db/url');
  18. const config = require('../config');
  19. const preservedUrls = [
  20. 'login',
  21. 'logout',
  22. 'signup',
  23. 'reset-password',
  24. 'resetpassword',
  25. 'url-password',
  26. 'settings',
  27. 'stats',
  28. 'verify',
  29. 'api',
  30. '404',
  31. 'static',
  32. 'images',
  33. ];
  34. exports.preservedUrls = preservedUrls;
  35. exports.urlShortener = async ({ body, user }, res) => {
  36. if (!body.target) return res.status(400).json({ error: 'No target has been provided.' });
  37. if (body.target.length > 1024) {
  38. return res.status(400).json({ error: 'Maximum URL length is 1024.' });
  39. }
  40. const isValidUrl = urlRegex({ exact: true, strict: false }).test(body.target);
  41. if (!isValidUrl) return res.status(400).json({ error: 'URL is not valid.' });
  42. const hasProtocol = /^https?/.test(URL.parse(body.target).protocol);
  43. const target = hasProtocol ? body.target : `http://${body.target}`;
  44. if (body.password && body.password.length > 64) {
  45. return res.status(400).json({ error: 'Maximum password length is 64.' });
  46. }
  47. if (user && body.customurl) {
  48. if (!/^[a-zA-Z1-9-_]+$/g.test(body.customurl.trim())) {
  49. return res.status(400).json({ error: 'Custom URL is not valid.' });
  50. }
  51. if (preservedUrls.some(url => url === body.customurl)) {
  52. return res.status(400).json({ error: "You can't use this custom URL name." });
  53. }
  54. if (body.customurl.length > 64) {
  55. return res.status(400).json({ error: 'Maximum custom URL length is 64.' });
  56. }
  57. const urls = await findUrl({ id: body.customurl || '' });
  58. if (urls.length) {
  59. const urlWithNoDomain = !user.domain && urls.some(url => !url.domain);
  60. const urlWithDmoain = user.domain && urls.some(url => url.domain === user.domain);
  61. if (urlWithNoDomain || urlWithDmoain) {
  62. return res.status(400).json({ error: 'Custom URL is already in use.' });
  63. }
  64. }
  65. }
  66. const isMalware = await axios.post(
  67. `https://safebrowsing.googleapis.com/v4/threatMatches:find?key=${
  68. config.GOOGLE_SAFE_BROWSING_KEY
  69. }`,
  70. {
  71. client: {
  72. clientId: config.DEFAULT_DOMAIN.toLowerCase().replace('.', ''),
  73. clientVersion: '1.0.0',
  74. },
  75. threatInfo: {
  76. threatTypes: ['MALWARE', 'SOCIAL_ENGINEERING'],
  77. platformTypes: ['WINDOWS'],
  78. threatEntryTypes: ['URL'],
  79. threatEntries: [{ url: body.target }],
  80. },
  81. }
  82. );
  83. if (isMalware.data && isMalware.data.matches) {
  84. return res.status(400).json({ error: 'Malware detected!' });
  85. }
  86. const url = await createShortUrl({ ...body, target, user });
  87. return res.json(url);
  88. };
  89. const browsersList = ['IE', 'Firefox', 'Chrome', 'Opera', 'Safari', 'Edge'];
  90. const osList = ['Windows', 'Mac Os X', 'Linux', 'Chrome OS', 'Android', 'iOS'];
  91. const botList = ['bot', 'dataminr', 'pinterest', 'yahoo', 'facebook', 'crawl'];
  92. const filterInBrowser = agent => item =>
  93. agent.family.toLowerCase().includes(item.toLocaleLowerCase());
  94. const filterInOs = agent => item =>
  95. agent.os.family.toLowerCase().includes(item.toLocaleLowerCase());
  96. exports.goToUrl = async (req, res, next) => {
  97. const { host } = req.headers;
  98. const id = req.params.id || req.body.id;
  99. const domain = host !== config.DEFAULT_DOMAIN && host;
  100. const agent = useragent.parse(req.headers['user-agent']);
  101. const [browser = 'Other'] = browsersList.filter(filterInBrowser(agent));
  102. const [os = 'Other'] = osList.filter(filterInOs(agent));
  103. const referrer = req.header('Referer') && URL.parse(req.header('Referer')).hostname;
  104. const location = geoip.lookup(req.realIp);
  105. const country = location && location.country;
  106. const urls = await findUrl({ id, domain });
  107. const isBot =
  108. botList.some(bot => agent.source.toLowerCase().includes(bot)) || agent.family === 'Other';
  109. if (!urls && !urls.length) return next();
  110. const [url] = urls;
  111. if (url.password && !req.body.password) {
  112. req.protectedUrl = id;
  113. return next();
  114. }
  115. if (url.password) {
  116. const isMatch = await bcrypt.compare(req.body.password, url.password);
  117. if (!isMatch) {
  118. return res.status(401).json({ error: 'Password is not correct' });
  119. }
  120. if (url.user && !isBot) {
  121. await createVisit({
  122. browser,
  123. country: country || 'Unknown',
  124. domain,
  125. id: url.id,
  126. os,
  127. referrer: referrer || 'Direct',
  128. });
  129. }
  130. return res.status(200).json({ target: url.target });
  131. }
  132. if (url.user && !isBot) {
  133. await createVisit({
  134. browser,
  135. country: country || 'Unknown',
  136. domain,
  137. id: url.id,
  138. os,
  139. referrer: referrer || 'Direct',
  140. });
  141. }
  142. return res.redirect(url.target);
  143. };
  144. exports.getUrls = async ({ query, user }, res) => {
  145. const urlsList = await getUrls({ options: query, user });
  146. return res.json(urlsList);
  147. };
  148. exports.setCustomDomain = async ({ body: { customDomain }, user }, res) => {
  149. if (customDomain.length > 40) {
  150. return res.status(400).json({ error: 'Maximum custom domain length is 40.' });
  151. }
  152. if (customDomain === config.DEFAULT_DOMAIN) {
  153. return res.status(400).json({ error: "You can't use default domain." });
  154. }
  155. const isValidDomain = urlRegex({ exact: true, strict: false }).test(customDomain);
  156. if (!isValidDomain) return res.status(400).json({ error: 'Domain is not valid.' });
  157. const isOwned = await getCustomDomain({ customDomain });
  158. if (isOwned && isOwned.email !== user.email) {
  159. return res
  160. .status(400)
  161. .json({ error: 'Domain is already taken. Contact us for multiple users.' });
  162. }
  163. const userCustomDomain = await setCustomDomain({ user, customDomain });
  164. if (userCustomDomain) return res.status(201).json({ customDomain: userCustomDomain.name });
  165. return res.status(400).json({ error: "Couldn't set custom domain." });
  166. };
  167. exports.deleteCustomDomain = async ({ user }, res) => {
  168. const response = await deleteCustomDomain({ user });
  169. if (response) return res.status(200).json({ message: 'Domain deleted successfully' });
  170. return res.status(400).json({ error: "Couldn't delete custom domain." });
  171. };
  172. exports.deleteUrl = async ({ body: { id, domain }, user }, res) => {
  173. if (!id) return res.status(400).json({ error: 'No id has been provided.' });
  174. const customDomain = domain !== config.DEFAULT_DOMAIN && domain;
  175. const urls = await findUrl({ id, domain: customDomain });
  176. if (!urls && !urls.length) return res.status(400).json({ error: "Couldn't find the short URL." });
  177. const response = await deleteUrl({ id, domain: customDomain, user });
  178. if (response) return res.status(200).json({ message: 'Sort URL deleted successfully' });
  179. return res.status(400).json({ error: "Couldn't delete short URL." });
  180. };
  181. exports.getStats = async ({ query: { id, domain }, user }, res) => {
  182. if (!id) return res.status(400).json({ error: 'No id has been provided.' });
  183. const customDomain = domain !== config.DEFAULT_DOMAIN && domain;
  184. const stats = await getStats({ id, domain: customDomain, user });
  185. if (!stats) return res.status(400).json({ error: 'Could not get the short URL stats.' });
  186. return res.status(200).json(stats);
  187. };