urlController.js 12 KB

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