| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428 |
- import { Handler } from 'express';
- import { promisify } from 'util';
- import urlRegex from 'url-regex';
- import dns from 'dns';
- import URL from 'url';
- import generate from 'nanoid/generate';
- import useragent from 'useragent';
- import geoip from 'geoip-lite';
- import bcrypt from 'bcryptjs';
- import ua from 'universal-analytics';
- import isbot from 'isbot';
- import { addIP } from '../db/ip';
- import {
- addLinkCount,
- createShortLink,
- createVisit,
- deleteLink,
- findLink,
- getUserLinksCount,
- getStats,
- getLinks,
- banLink,
- } from '../db/link';
- import {
- checkBannedDomain,
- checkBannedHost,
- cooldownCheck,
- malwareCheck,
- preservedUrls,
- urlCountsCheck,
- } from './validateBodyController';
- import { getDomain, setDomain, deleteDomain } from '../db/domain';
- import transporter from '../mail/mail';
- import * as redis from '../redis';
- import {
- addProtocol,
- getStatsLimit,
- generateShortLink,
- getStatsCacheTime,
- } from '../utils';
- import { IDomain } from '../models/domain';
- const dnsLookup = promisify(dns.lookup);
- const generateId = async () => {
- const id = generate(
- 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890',
- 6
- );
- const link = await findLink({ id });
- if (!link) return id;
- return generateId();
- };
- export const shortener: Handler = async (req, res) => {
- try {
- const targetDomain = URL.parse(req.body.target).hostname;
- const queries = await Promise.all([
- process.env.GOOGLE_SAFE_BROWSING_KEY && cooldownCheck(req.user),
- process.env.GOOGLE_SAFE_BROWSING_KEY &&
- malwareCheck(req.user, req.body.target),
- req.user && urlCountsCheck(req.user._id),
- req.user &&
- req.body.reuse &&
- findLink({ target: addProtocol(req.body.target), user: req.user._id }),
- req.user &&
- req.body.customurl &&
- findLink(
- {
- id: req.body.customurl,
- domain: req.user.domain && req.user.domain._id,
- },
- { forceDomainCheck: true }
- ),
- (!req.user || !req.body.customurl) && generateId(),
- checkBannedDomain(targetDomain),
- checkBannedHost(targetDomain),
- ]);
- // if "reuse" is true, try to return
- // the existent URL without creating one
- if (queries[3]) {
- const { domain: d, user: u, ...link } = queries[3];
- const data = {
- ...link,
- password: !!link.password,
- reuse: true,
- shortLink: generateShortLink(link.id, req.user.domain),
- };
- return res.json(data);
- }
- // Check if custom link already exists
- if (queries[4]) {
- throw new Error('Custom URL is already in use.');
- }
- // Create new link
- const id = (req.user && req.body.customurl) || queries[5];
- const target = addProtocol(req.body.target);
- const link = await createShortLink({
- ...req.body,
- id,
- target,
- user: req.user,
- });
- if (!req.user && Number(process.env.NON_USER_COOLDOWN)) {
- addIP(req.realIP);
- }
- return res.json(link);
- } catch (error) {
- return res.status(400).json({ error: error.message });
- }
- };
- const browsersList = ['IE', 'Firefox', 'Chrome', 'Opera', 'Safari', 'Edge'];
- const osList = ['Windows', 'Mac Os', 'Linux', 'Android', 'iOS'];
- const filterInBrowser = agent => item =>
- agent.family.toLowerCase().includes(item.toLocaleLowerCase());
- const filterInOs = agent => item =>
- agent.os.family.toLowerCase().includes(item.toLocaleLowerCase());
- export const goToLink: Handler = async (req, res, next) => {
- const { host } = req.headers;
- const reqestedId = req.params.id || req.body.id;
- const id = reqestedId.replace('+', '');
- const customDomain = host !== process.env.DEFAULT_DOMAIN && host;
- const agent = useragent.parse(req.headers['user-agent']);
- const [browser = 'Other'] = browsersList.filter(filterInBrowser(agent));
- const [os = 'Other'] = osList.filter(filterInOs(agent));
- const referrer =
- req.header('Referer') && URL.parse(req.header('Referer')).hostname;
- const location = geoip.lookup(req.realIP);
- const country = location && location.country;
- const isBot = isbot(req.headers['user-agent']);
- const domain = await (customDomain && getDomain({ name: customDomain }));
- const link = await findLink({ id, domain: domain && domain._id });
- if (!link) {
- if (host !== process.env.DEFAULT_DOMAIN) {
- if (!domain || !domain.homepage) return next();
- return res.redirect(301, domain.homepage);
- }
- return next();
- }
- if (link.banned) {
- return res.redirect('/banned');
- }
- const doesRequestInfo = /.*\+$/gi.test(reqestedId);
- if (doesRequestInfo && !link.password) {
- req.linkTarget = link.target;
- req.pageType = 'info';
- return next();
- }
- if (link.password && !req.body.password) {
- req.protectedLink = id;
- req.pageType = 'password';
- return next();
- }
- if (link.password) {
- const isMatch = await bcrypt.compare(req.body.password, link.password);
- if (!isMatch) {
- return res.status(401).json({ error: 'Password is not correct' });
- }
- if (link.user && !isBot) {
- addLinkCount(link.id, customDomain);
- createVisit({
- browser: browser.toLowerCase(),
- country: country || 'Unknown',
- domain: customDomain,
- id: link.id,
- os: os.toLowerCase().replace(/\s/gi, ''),
- referrer: referrer.replace(/\./gi, '[dot]') || 'Direct',
- limit: getStatsLimit(),
- });
- }
- return res.status(200).json({ target: link.target });
- }
- if (link.user && !isBot) {
- addLinkCount(link.id, customDomain);
- createVisit({
- browser,
- country: country || 'Unknown',
- domain: customDomain,
- id: link.id,
- os,
- referrer: referrer || 'Direct',
- limit: getStatsLimit(),
- });
- }
- if (process.env.GOOGLE_ANALYTICS_UNIVERSAL && !isBot) {
- const visitor = ua(process.env.GOOGLE_ANALYTICS_UNIVERSAL);
- visitor
- .pageview({
- dp: `/${id}`,
- ua: req.headers['user-agent'],
- uip: req.realIP,
- aip: 1,
- })
- .send();
- }
- return res.redirect(link.target);
- };
- export const getUserLinks: Handler = async (req, res) => {
- // TODO: Use aggregation
- const [countAll, list] = await Promise.all([
- getUserLinksCount({ user: req.user }),
- getLinks(req.user, req.query),
- ]);
- return res.json({ list, countAll });
- };
- export const setCustomDomain: Handler = async (req, res) => {
- const parsed = URL.parse(req.body.customDomain);
- const customDomain = parsed.hostname || parsed.href;
- if (!customDomain)
- return res.status(400).json({ error: 'Domain is not valid.' });
- if (customDomain.length > 40) {
- return res
- .status(400)
- .json({ error: 'Maximum custom domain length is 40.' });
- }
- if (customDomain === process.env.DEFAULT_DOMAIN) {
- return res.status(400).json({ error: "You can't use default domain." });
- }
- const isValidHomepage =
- !req.body.homepage ||
- urlRegex({ exact: true, strict: false }).test(req.body.homepage);
- if (!isValidHomepage)
- return res.status(400).json({ error: 'Homepage is not valid.' });
- const homepage =
- req.body.homepage &&
- (URL.parse(req.body.homepage).protocol
- ? req.body.homepage
- : `http://${req.body.homepage}`);
- const matchedDomain = await getDomain({ name: customDomain });
- if (
- matchedDomain &&
- matchedDomain.user.toString() !== req.user._id.toString()
- ) {
- return res.status(400).json({
- error: 'Domain is already taken. Contact us for multiple users.',
- });
- }
- const userCustomDomain = await setDomain({
- user: req.user,
- name: customDomain,
- homepage,
- });
- if (userCustomDomain) {
- return res.status(201).json({
- customDomain: userCustomDomain.name,
- homepage: userCustomDomain.homepage,
- });
- }
- return res.status(400).json({ error: "Couldn't set custom domain." });
- };
- export const deleteCustomDomain: Handler = async (req, res) => {
- const response = await deleteDomain(req.user);
- if (response)
- return res.status(200).json({ message: 'Domain deleted successfully' });
- return res.status(400).json({ error: "Couldn't delete custom domain." });
- };
- export const customDomainRedirection: Handler = async (req, res, next) => {
- const { headers, path } = req;
- if (
- headers.host !== process.env.DEFAULT_DOMAIN &&
- (path === '/' ||
- preservedUrls
- .filter(l => l !== 'url-password')
- .some(item => item === path.replace('/', '')))
- ) {
- const domain = await getDomain({ name: headers.host });
- return res.redirect(
- 301,
- (domain && domain.homepage) ||
- `https://${process.env.DEFAULT_DOMAIN + path}`
- );
- }
- return next();
- };
- export const deleteUserLink: Handler = async (req, res) => {
- if (!req.body.id)
- return res.status(400).json({ error: 'No id has been provided.' });
- const customDomain =
- req.body.domain !== process.env.DEFAULT_DOMAIN
- ? await getDomain({ name: req.body.domain })
- : ({} as IDomain);
- const link = await findLink(
- {
- id: req.body.id,
- domain: customDomain._id,
- user: req.user && req.user._id,
- },
- { forceDomainCheck: true }
- );
- if (!link) {
- return res.status(400).json({ error: "Couldn't find the short link." });
- }
- const response = await deleteLink({
- id: req.body,
- domain: customDomain._id,
- user: req.user,
- });
- if (response) {
- return res.status(200).json({ message: 'Short link deleted successfully' });
- }
- return res.status(400).json({ error: "Couldn't delete the short link." });
- };
- export const getLinkStats: Handler = async (req, res) => {
- if (!req.query.id)
- return res.status(400).json({ error: 'No id has been provided.' });
- const customDomain =
- req.query.domain !== process.env.DEFAULT_DOMAIN
- ? await getDomain({ name: req.query.domain })
- : ({} as IDomain);
- const redisKey = req.query.id + (customDomain.name || '') + req.user.email;
- const cached = await redis.get(redisKey);
- if (cached) return res.status(200).json(JSON.parse(cached));
- const link = await findLink(
- {
- id: req.query.id,
- domain: customDomain._id,
- user: req.user && req.user._id,
- },
- { forceDomainCheck: true }
- );
- if (!link)
- return res.status(400).json({ error: "Couldn't find the short link." });
- const stats = await getStats({
- id: req.query.id,
- domain: customDomain._id,
- user: req.user,
- });
- if (!stats) {
- return res
- .status(400)
- .json({ error: 'Could not get the short link stats.' });
- }
- const cacheTime = getStatsCacheTime(stats.total);
- redis.set(redisKey, JSON.stringify(stats), 'EX', cacheTime);
- return res.status(200).json(stats);
- };
- export const reportLink: Handler = async (req, res) => {
- // TODO: Change from url to link in front-end
- if (!req.body.link)
- return res.status(400).json({ error: 'No URL has been provided.' });
- const { hostname } = URL.parse(req.body.link);
- if (hostname !== process.env.DEFAULT_DOMAIN) {
- return res.status(400).json({
- error: `You can only report a ${process.env.DEFAULT_DOMAIN} link`,
- });
- }
- const mail = await transporter.sendMail({
- from: process.env.MAIL_USER,
- to: process.env.REPORT_MAIL,
- subject: '[REPORT]',
- text: req.body.url,
- html: req.body.url,
- });
- if (mail.accepted.length) {
- return res
- .status(200)
- .json({ message: "Thanks for the report, we'll take actions shortly." });
- }
- return res
- .status(400)
- .json({ error: "Couldn't submit the report. Try again later." });
- };
- export const ban: Handler = async (req, res) => {
- if (!req.body.id)
- return res.status(400).json({ error: 'No id has been provided.' });
- const link = await findLink({ id: req.body.id }, { forceDomainCheck: true });
- if (!link) return res.status(400).json({ error: "Couldn't find the link." });
- if (link.banned) {
- return res.status(200).json({ message: 'Link was banned already' });
- }
- const domain = URL.parse(link.target).hostname;
- let host;
- if (req.body.host) {
- try {
- const dnsRes = await dnsLookup(domain);
- host = dnsRes && dnsRes.address;
- } catch (error) {
- host = null;
- }
- }
- await banLink({
- adminId: req.user,
- domain,
- host,
- id: req.body.id,
- banUser: !!req.body.user,
- });
- return res.status(200).json({ message: 'Link has been banned successfully' });
- };
|