| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471 |
- const { body, param } = require("express-validator");
- const { isAfter, subDays, subHours, addMilliseconds } = require("date-fns");
- const urlRegex = require("url-regex-safe");
- const { promisify } = require("util");
- const bcrypt = require("bcryptjs");
- const axios = require("axios");
- const dns = require("dns");
- const URL = require("url");
- const ms = require("ms");
- const { CustomError, addProtocol, preservedURLs, removeWww } = require("../utils");
- const query = require("../queries");
- const knex = require("../knex");
- const env = require("../env");
- const dnsLookup = promisify(dns.lookup);
- const checkUser = (value, { req }) => !!req.user;
- const createLink = [
- body("target")
- .exists({ checkNull: true, checkFalsy: true })
- .withMessage("Target is missing.")
- .isString()
- .trim()
- .isLength({ min: 1, max: 2040 })
- .withMessage("Maximum URL length is 2040.")
- .customSanitizer(addProtocol)
- .custom(
- value =>
- urlRegex({ exact: true, strict: false }).test(value) ||
- /^(?!https?)(\w+):\/\//.test(value)
- )
- .withMessage("URL is not valid.")
- .custom(value => removeWww(URL.parse(value).host) !== env.DEFAULT_DOMAIN)
- .withMessage(`${env.DEFAULT_DOMAIN} URLs are not allowed.`),
- body("password")
- .optional({ nullable: true, checkFalsy: true })
- .custom(checkUser)
- .withMessage("Only users can use this field.")
- .isString()
- .isLength({ min: 3, max: 64 })
- .withMessage("Password length must be between 3 and 64."),
- body("customurl")
- .optional({ nullable: true, checkFalsy: true })
- .custom(checkUser)
- .withMessage("Only users can use this field.")
- .isString()
- .trim()
- .isLength({ min: 1, max: 64 })
- .withMessage("Custom URL length must be between 1 and 64.")
- .custom(value => /^[a-zA-Z0-9-_]+$/g.test(value))
- .withMessage("Custom URL is not valid")
- .custom(value => !preservedURLs.some(url => url.toLowerCase() === value))
- .withMessage("You can't use this custom URL."),
- body("reuse")
- .optional({ nullable: true })
- .custom(checkUser)
- .withMessage("Only users can use this field.")
- .isBoolean()
- .withMessage("Reuse must be boolean."),
- body("description")
- .optional({ nullable: true, checkFalsy: true })
- .isString()
- .trim()
- .isLength({ min: 0, max: 2040 })
- .withMessage("Description length must be between 0 and 2040."),
- body("expire_in")
- .optional({ nullable: true, checkFalsy: true })
- .isString()
- .trim()
- .custom(value => {
- try {
- return !!ms(value);
- } catch {
- return false;
- }
- })
- .withMessage("Expire format is invalid. Valid examples: 1m, 8h, 42 days.")
- .customSanitizer(ms)
- .custom(value => value >= ms("1m"))
- .withMessage("Minimum expire time should be '1 minute'.")
- .customSanitizer(value => addMilliseconds(new Date(), value).toISOString()),
- body("domain")
- .optional({ nullable: true, checkFalsy: true })
- .custom(checkUser)
- .withMessage("Only users can use this field.")
- .isString()
- .withMessage("Domain should be string.")
- .customSanitizer(value => value.toLowerCase())
- .customSanitizer(value => removeWww(URL.parse(value).hostname || value))
- .custom(async (address, { req }) => {
- if (address === env.DEFAULT_DOMAIN) {
- req.body.domain = null;
- return;
- }
- const domain = await query.domain.find({
- address,
- user_id: req.user.id
- });
- req.body.domain = domain || null;
- if (!domain) return Promise.reject();
- })
- .withMessage("You can't use this domain.")
- ];
- // export const editLink = [
- // body("target")
- // .optional({ checkFalsy: true, nullable: true })
- // .isString()
- // .trim()
- // .isLength({ min: 1, max: 2040 })
- // .withMessage("Maximum URL length is 2040.")
- // .customSanitizer(addProtocol)
- // .custom(
- // value =>
- // urlRegex({ exact: true, strict: false }).test(value) ||
- // /^(?!https?)(\w+):\/\//.test(value)
- // )
- // .withMessage("URL is not valid.")
- // .custom(value => removeWww(URL.parse(value).host) !== env.DEFAULT_DOMAIN)
- // .withMessage(`${env.DEFAULT_DOMAIN} URLs are not allowed.`),
- // body("password")
- // .optional({ nullable: true, checkFalsy: true })
- // .isString()
- // .isLength({ min: 3, max: 64 })
- // .withMessage("Password length must be between 3 and 64."),
- // body("address")
- // .optional({ checkFalsy: true, nullable: true })
- // .isString()
- // .trim()
- // .isLength({ min: 1, max: 64 })
- // .withMessage("Custom URL length must be between 1 and 64.")
- // .custom(value => /^[a-zA-Z0-9-_]+$/g.test(value))
- // .withMessage("Custom URL is not valid")
- // .custom(value => !preservedURLs.some(url => url.toLowerCase() === value))
- // .withMessage("You can't use this custom URL."),
- // body("expire_in")
- // .optional({ nullable: true, checkFalsy: true })
- // .isString()
- // .trim()
- // .custom(value => {
- // try {
- // return !!ms(value);
- // } catch {
- // return false;
- // }
- // })
- // .withMessage("Expire format is invalid. Valid examples: 1m, 8h, 42 days.")
- // .customSanitizer(ms)
- // .custom(value => value >= ms("1m"))
- // .withMessage("Minimum expire time should be '1 minute'.")
- // .customSanitizer(value => addMilliseconds(new Date(), value).toISOString()),
- // body("description")
- // .optional({ nullable: true, checkFalsy: true })
- // .isString()
- // .trim()
- // .isLength({ min: 0, max: 2040 })
- // .withMessage("Description length must be between 0 and 2040."),
- // param("id", "ID is invalid.")
- // .exists({ checkFalsy: true, checkNull: true })
- // .isLength({ min: 36, max: 36 })
- // ];
- // export const redirectProtected = [
- // body("password", "Password is invalid.")
- // .exists({ checkFalsy: true, checkNull: true })
- // .isString()
- // .isLength({ min: 3, max: 64 })
- // .withMessage("Password length must be between 3 and 64."),
- // param("id", "ID is invalid.")
- // .exists({ checkFalsy: true, checkNull: true })
- // .isLength({ min: 36, max: 36 })
- // ];
- // export const addDomain = [
- // body("address", "Domain is not valid")
- // .exists({ checkFalsy: true, checkNull: true })
- // .isLength({ min: 3, max: 64 })
- // .withMessage("Domain length must be between 3 and 64.")
- // .trim()
- // .customSanitizer(value => {
- // const parsed = URL.parse(value);
- // return removeWww(parsed.hostname || parsed.href);
- // })
- // .custom(value => urlRegex({ exact: true, strict: false }).test(value))
- // .custom(value => value !== env.DEFAULT_DOMAIN)
- // .withMessage("You can't use the default domain.")
- // .custom(async value => {
- // const domain = await query.domain.find({ address: value });
- // if (domain?.user_id || domain?.banned) return Promise.reject();
- // })
- // .withMessage("You can't add this domain."),
- // body("homepage")
- // .optional({ checkFalsy: true, nullable: true })
- // .customSanitizer(addProtocol)
- // .custom(value => urlRegex({ exact: true, strict: false }).test(value))
- // .withMessage("Homepage is not valid.")
- // ];
- // export const removeDomain = [
- // param("id", "ID is invalid.")
- // .exists({
- // checkFalsy: true,
- // checkNull: true
- // })
- // .isLength({ min: 36, max: 36 })
- // ];
- // export const deleteLink = [
- // param("id", "ID is invalid.")
- // .exists({
- // checkFalsy: true,
- // checkNull: true
- // })
- // .isLength({ min: 36, max: 36 })
- // ];
- // export const reportLink = [
- // body("link", "No link has been provided.")
- // .exists({
- // checkFalsy: true,
- // checkNull: true
- // })
- // .customSanitizer(addProtocol)
- // .custom(
- // value => removeWww(URL.parse(value).hostname) === env.DEFAULT_DOMAIN
- // )
- // .withMessage(`You can only report a ${env.DEFAULT_DOMAIN} link.`)
- // ];
- // export const banLink = [
- // param("id", "ID is invalid.")
- // .exists({
- // checkFalsy: true,
- // checkNull: true
- // })
- // .isLength({ min: 36, max: 36 }),
- // body("host", '"host" should be a boolean.')
- // .optional({
- // nullable: true
- // })
- // .isBoolean(),
- // body("user", '"user" should be a boolean.')
- // .optional({
- // nullable: true
- // })
- // .isBoolean(),
- // body("userlinks", '"userlinks" should be a boolean.')
- // .optional({
- // nullable: true
- // })
- // .isBoolean(),
- // body("domain", '"domain" should be a boolean.')
- // .optional({
- // nullable: true
- // })
- // .isBoolean()
- // ];
- // export const getStats = [
- // param("id", "ID is invalid.")
- // .exists({
- // checkFalsy: true,
- // checkNull: true
- // })
- // .isLength({ min: 36, max: 36 })
- // ];
- const signup = [
- body("password", "Password is not valid.")
- .exists({ checkFalsy: true, checkNull: true })
- .isLength({ min: 8, max: 64 })
- .withMessage("Password length must be between 8 and 64."),
- body("email", "Email is not valid.")
- .exists({ checkFalsy: true, checkNull: true })
- .trim()
- .isEmail()
- .isLength({ min: 0, max: 255 })
- .withMessage("Email length must be max 255.")
- .custom(async (value, { req }) => {
- const user = await query.user.find({ email: value });
- if (user) {
- req.user = user;
- }
- if (user?.verified) return Promise.reject();
- })
- .withMessage("You can't use this email address.")
- ];
- const login = [
- body("password", "Password is not valid.")
- .exists({ checkFalsy: true, checkNull: true })
- .isLength({ min: 8, max: 64 })
- .withMessage("Password length must be between 8 and 64."),
- body("email", "Email is not valid.")
- .exists({ checkFalsy: true, checkNull: true })
- .trim()
- .isEmail()
- .isLength({ min: 0, max: 255 })
- .withMessage("Email length must be max 255.")
- ];
- // export const changePassword = [
- // body("password", "Password is not valid.")
- // .exists({ checkFalsy: true, checkNull: true })
- // .isLength({ min: 8, max: 64 })
- // .withMessage("Password length must be between 8 and 64.")
- // ];
- // export const resetPasswordRequest = [
- // body("email", "Email is not valid.")
- // .exists({ checkFalsy: true, checkNull: true })
- // .trim()
- // .isEmail()
- // .isLength({ min: 0, max: 255 })
- // .withMessage("Email length must be max 255."),
- // body("password", "Password is not valid.")
- // .exists({ checkFalsy: true, checkNull: true })
- // .isLength({ min: 8, max: 64 })
- // .withMessage("Password length must be between 8 and 64.")
- // ];
- // export const resetEmailRequest = [
- // body("email", "Email is not valid.")
- // .exists({ checkFalsy: true, checkNull: true })
- // .trim()
- // .isEmail()
- // .isLength({ min: 0, max: 255 })
- // .withMessage("Email length must be max 255.")
- // ];
- // export const deleteUser = [
- // body("password", "Password is not valid.")
- // .exists({ checkFalsy: true, checkNull: true })
- // .isLength({ min: 8, max: 64 })
- // .custom(async (password, { req }) => {
- // const isMatch = await bcrypt.compare(password, req.user.password);
- // if (!isMatch) return Promise.reject();
- // })
- // ];
- // TODO: if user has posted malware should do something better
- function cooldown(user) {
- if (!env.GOOGLE_SAFE_BROWSING_KEY || !user || !user.cooldowns) return;
- // If has active cooldown then throw error
- const hasCooldownNow = user.cooldowns.some(cooldown =>
- isAfter(subHours(new Date(), 12), new Date(cooldown))
- );
- if (hasCooldownNow) {
- throw new CustomError("Cooldown because of a malware URL. Wait 12h");
- }
- }
- // TODO: if user or non-user has posted malware should do something better
- async function malware(user, target) {
- if (!env.GOOGLE_SAFE_BROWSING_KEY) return;
- const isMalware = await axios.post(
- `https://safebrowsing.googleapis.com/v4/threatMatches:find?key=${env.GOOGLE_SAFE_BROWSING_KEY}`,
- {
- client: {
- clientId: 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) return;
- if (user) {
- const [updatedUser] = await query.user.update(
- { id: user.id },
- {
- cooldowns: knex.raw("array_append(cooldowns, ?)", [
- new Date().toISOString()
- ])
- }
- );
- // Ban if too many cooldowns
- if (updatedUser.cooldowns.length > 2) {
- await query.user.update({ id: user.id }, { banned: true });
- throw new CustomError("Too much malware requests. You are now banned.");
- }
- }
- throw new CustomError(
- user ? "Malware detected! Cooldown for 12h." : "Malware detected!"
- );
- };
- async function linksCount(user) {
- if (!user) return;
- const count = await query.link.total({
- user_id: user.id,
- created_at: [">", subDays(new Date(), 1).toISOString()]
- });
- if (count > env.USER_LIMIT_PER_DAY) {
- throw new CustomError(
- `You have reached your daily limit (${env.USER_LIMIT_PER_DAY}). Please wait 24h.`
- );
- }
- };
- async function bannedDomain(domain) {
- const isBanned = await query.domain.find({
- address: domain,
- banned: true
- });
- if (isBanned) {
- throw new CustomError("URL is containing malware/scam.", 400);
- }
- };
- async function bannedHost(domain) {
- let isBanned;
- try {
- const dnsRes = await dnsLookup(domain);
- if (!dnsRes || !dnsRes.address) return;
- isBanned = await query.host.find({
- address: dnsRes.address,
- banned: true
- });
- } catch (error) {
- isBanned = null;
- }
- if (isBanned) {
- throw new CustomError("URL is containing malware/scam.", 400);
- }
- };
- module.exports = {
- bannedDomain,
- bannedHost,
- checkUser,
- cooldown,
- createLink,
- linksCount,
- login,
- malware,
- signup,
- }
|