| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229 |
- import { RequestHandler } from 'express';
- import { promisify } from 'util';
- import dns from 'dns';
- import axios from 'axios';
- import URL from 'url';
- import urlRegex from 'url-regex';
- import validator from 'express-validator/check';
- import { differenceInMinutes, subHours, subDays } from 'date-fns';
- import { validationResult } from 'express-validator/check';
- import { IUser } from '../models/user';
- import { addCooldown, banUser } from '../db/user';
- import { getIP } from '../db/ip';
- import { getUserLinksCount } from '../db/link';
- import { getDomain } from '../db/domain';
- import { getHost } from '../db/host';
- import { addProtocol } from '../utils';
- const dnsLookup = promisify(dns.lookup);
- export const validationCriterias = [
- validator
- .body('email')
- .exists()
- .withMessage('Email must be provided.')
- .isEmail()
- .withMessage('Email is not valid.')
- .trim()
- .normalizeEmail(),
- validator
- .body('password', 'Password must be at least 8 chars long.')
- .exists()
- .withMessage('Password must be provided.')
- .isLength({ min: 8 }),
- ];
- export const validateBody = (req, res, next) => {
- const errors = validationResult(req);
- if (!errors.isEmpty()) {
- const errorsObj = errors.mapped();
- const emailError = errorsObj.email && errorsObj.email.msg;
- const passwordError = errorsObj.password && errorsObj.password.msg;
- return res.status(400).json({ error: emailError || passwordError });
- }
- return next();
- };
- export const preservedUrls = [
- 'login',
- 'logout',
- 'signup',
- 'reset-password',
- 'resetpassword',
- 'url-password',
- 'url-info',
- 'settings',
- 'stats',
- 'verify',
- 'api',
- '404',
- 'static',
- 'images',
- 'banned',
- 'terms',
- 'privacy',
- 'report',
- 'pricing',
- ];
- export const validateUrl: RequestHandler = async (req, res, next) => {
- // Validate URL existence
- if (!req.body.target)
- return res.status(400).json({ error: 'No target has been provided.' });
- // validate URL length
- if (req.body.target.length > 3000) {
- return res.status(400).json({ error: 'Maximum URL length is 3000.' });
- }
- // Validate URL
- const isValidUrl = urlRegex({ exact: true, strict: false }).test(
- req.body.target
- );
- if (!isValidUrl && !/^\w+:\/\//.test(req.body.target))
- return res.status(400).json({ error: 'URL is not valid.' });
- // If target is the URL shortener itself
- const { host } = URL.parse(addProtocol(req.body.target));
- if (host === process.env.DEFAULT_DOMAIN) {
- return res
- .status(400)
- .json({ error: `${process.env.DEFAULT_DOMAIN} URLs are not allowed.` });
- }
- // Validate password length
- if (req.body.password && req.body.password.length > 64) {
- return res.status(400).json({ error: 'Maximum password length is 64.' });
- }
- // Custom URL validations
- if (req.user && req.body.customurl) {
- // Validate custom URL
- if (!/^[a-zA-Z0-9-_]+$/g.test(req.body.customurl.trim())) {
- return res.status(400).json({ error: 'Custom URL is not valid.' });
- }
- // Prevent from using preserved URLs
- if (preservedUrls.some(url => url === req.body.customurl)) {
- return res
- .status(400)
- .json({ error: "You can't use this custom URL name." });
- }
- // Validate custom URL length
- if (req.body.customurl.length > 64) {
- return res
- .status(400)
- .json({ error: 'Maximum custom URL length is 64.' });
- }
- }
- return next();
- };
- export const cooldownCheck = async (user: IUser) => {
- if (user && user.cooldowns) {
- if (user.cooldowns.length > 4) {
- await banUser(user._id);
- throw new Error('Too much malware requests. You are now banned.');
- }
- const hasCooldownNow = user.cooldowns.some(
- cooldown => cooldown.toJSON() > subHours(new Date(), 12).toJSON()
- );
- if (hasCooldownNow) {
- throw new Error('Cooldown because of a malware URL. Wait 12h');
- }
- }
- };
- export const ipCooldownCheck: RequestHandler = async (req, res, next) => {
- const cooldownConfig = Number(process.env.NON_USER_COOLDOWN);
- if (req.user || !cooldownConfig) return next();
- const ip = await getIP(req.realIP);
- if (ip) {
- const timeToWait =
- cooldownConfig - differenceInMinutes(new Date(), ip.createdAt);
- return res.status(400).json({
- error:
- `Non-logged in users are limited. Wait ${timeToWait} ` +
- 'minutes or log in.',
- });
- }
- next();
- };
- export const malwareCheck = async (user: IUser, target: string) => {
- const isMalware = await axios.post(
- `https://safebrowsing.googleapis.com/v4/threatMatches:find?key=${
- process.env.GOOGLE_SAFE_BROWSING_KEY
- }`,
- {
- client: {
- clientId: process.env.DEFAULT_DOMAIN.toLowerCase().replace('.', ''),
- clientVersion: '1.0.0',
- },
- threatInfo: {
- threatTypes: [
- 'THREAT_TYPE_UNSPECIFIED',
- 'MALWARE',
- 'SOCIAL_ENGINEERING',
- 'UNWANTED_SOFTWARE',
- 'POTENTIALLY_HARMFUL_APPLICATION',
- ],
- platformTypes: ['ANY_PLATFORM', 'PLATFORM_TYPE_UNSPECIFIED'],
- threatEntryTypes: [
- 'EXECUTABLE',
- 'URL',
- 'THREAT_ENTRY_TYPE_UNSPECIFIED',
- ],
- threatEntries: [{ url: target }],
- },
- }
- );
- if (isMalware.data && isMalware.data.matches) {
- if (user) {
- await addCooldown(user._id);
- }
- throw new Error(
- user ? 'Malware detected! Cooldown for 12h.' : 'Malware detected!'
- );
- }
- };
- export const urlCountsCheck = async (user: IUser) => {
- const count = await getUserLinksCount({
- user: user._id,
- date: subDays(new Date(), 1),
- });
- if (count > Number(process.env.USER_LIMIT_PER_DAY)) {
- throw new Error(
- `You have reached your daily limit (${
- process.env.USER_LIMIT_PER_DAY
- }). Please wait 24h.`
- );
- }
- };
- export const checkBannedDomain = async (domain: string) => {
- const bannedDomain = await getDomain({ name: domain, banned: true });
- if (bannedDomain) {
- throw new Error('URL is containing malware/scam.');
- }
- };
- export const checkBannedHost = async (domain: string) => {
- let isHostBanned;
- try {
- const dnsRes = await dnsLookup(domain);
- isHostBanned = await getHost({
- address: dnsRes && dnsRes.address,
- banned: true,
- });
- } catch (error) {
- isHostBanned = null;
- }
- if (isHostBanned) {
- throw new Error('URL is containing malware/scam.');
- }
- };
|