UNPKG

moonito

Version:

Moonito is a simple but powerful real-time web traffic filtering service and analytics that not only helps you to track, analyse and understand your visitors but also protects your website from bot attacks, data scraping, and other unwanted activities.

274 lines (273 loc) 12.7 kB
"use strict"; var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k); __setModuleDefault(result, mod); return result; }; var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } return new (P || (P = Promise))(function (resolve, reject) { function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } step((generator = generator.apply(thisArg, _arguments || [])).next()); }); }; Object.defineProperty(exports, "__esModule", { value: true }); exports.VisitorTrafficFiltering = void 0; const https = __importStar(require("https")); const net = __importStar(require("net")); const querystring = __importStar(require("querystring")); const url_1 = require("url"); class VisitorTrafficFiltering { /** * Creates an instance of AnalyticsHandler. * @param config - The configuration for the handler, including protection settings and API keys. */ constructor(config) { this.config = config; } /** * Handles visitor requests by checking IP address and interacting with the analytics API. * This method: * 1. Checks if protection is enabled. * 2. Retrieves and validates the client's IP address and other request details. * 3. Makes a request to the analytics API to check if the visitor should be blocked. * 4. Takes action based on the API response and configuration, such as redirecting or displaying content. * * @param req - The request object, typically from an Express.js application. * @param res - The response object, typically from an Express.js application. * @returns {Promise<void>} A promise that resolves when the response is sent. * @throws {Error} Throws an error if there's an issue with the IP address or the API request. */ evaluateVisitor(req, res) { return __awaiter(this, void 0, void 0, function* () { var _a, _b; if (!this.config.isProtected) { return; } const clientIp = req.headers['x-forwarded-for'] || req.connection.remoteAddress; const userAgent = req.headers['user-agent']; const url = req.url; const domain = req.hostname.toLowerCase(); if (!this.isValidIp(clientIp)) { throw new Error("Invalid IP address."); } try { const response = yield this.requestAnalyticsAPI(clientIp, userAgent, url, domain); const data = JSON.parse(response); if (data.error) { throw new Error(`Requesting analytics error: ${Array.isArray(data.error.message) ? data.error.message.join(', ') : data.error.message}`); } if ((_b = (_a = data === null || data === void 0 ? void 0 : data.data) === null || _a === void 0 ? void 0 : _a.status) === null || _b === void 0 ? void 0 : _b.need_to_block) { this.handleBlockedVisitor(res); } } catch (error) { console.error('Error handling visitor:', error); throw new Error(`Error handling visitor: ${error.message}`); } }); } /** * Manually handles visitor data using provided IP address, user agent, and event. * * @param ip - The IP address of the visitor. * @param userAgent - The user agent string of the visitor. * @param event - The event associated with the visitor. * @param domain - The domain to be sent to the analytics API. * @returns {Promise<string>} The response content for blocked visitors. * @throws {Error} Throws an error if there's an issue with the IP address or the API request. */ evaluateVisitorManually(ip, userAgent, event, domain) { return __awaiter(this, void 0, void 0, function* () { var _a, _b, _c, _d; if (!this.config.isProtected) { return; } if (!this.isValidIp(ip)) { throw new Error("Invalid IP address."); } try { const response = yield this.requestAnalyticsAPI(ip, userAgent, event, domain); const data = JSON.parse(response); if (data.error) { throw new Error(`Requesting analytics error: ${Array.isArray(data.error.message) ? data.error.message.join(', ') : data.error.message}`); } const needToBlock = (_b = (_a = data === null || data === void 0 ? void 0 : data.data) === null || _a === void 0 ? void 0 : _a.status) === null || _b === void 0 ? void 0 : _b.need_to_block; const detectActivity = (_d = (_c = data === null || data === void 0 ? void 0 : data.data) === null || _c === void 0 ? void 0 : _c.status) === null || _d === void 0 ? void 0 : _d.detect_activity; if (needToBlock) { return { need_to_block: true, detect_activity: detectActivity, content: this.getBlockedContent() }; } return { need_to_block: false, detect_activity: detectActivity, content: null }; } catch (error) { console.error('Error handling visitor manually:', error); throw new Error(`Error handling visitor manually: ${error.message}`); } }); } /** * Makes a request to the analytics API. * @param ip - The IP address to query. * @param userAgent - The user agent to send. * @param event - The event to query. * @param domain - The domain to send. * @returns {Promise<string>} The response body from the API. */ requestAnalyticsAPI(ip, userAgent, event, domain) { return __awaiter(this, void 0, void 0, function* () { const queryParams = querystring.stringify({ ip, ua: encodeURIComponent(userAgent), events: encodeURIComponent(event), domain }); const url = new url_1.URL(`https://moonito.net/api/v1/analytics?${queryParams}`); const options = { method: 'GET', headers: { 'User-Agent': userAgent, 'X-Public-Key': this.config.apiPublicKey, 'X-Secret-Key': this.config.apiSecretKey, }, }; return this.httpRequest(url, options); }); } /** * Handles blocked visitors based on the configured action. * @param res - The response object. */ handleBlockedVisitor(res) { if (this.config.unwantedVisitorTo) { const statusCode = Number(this.config.unwantedVisitorTo); if (!isNaN(statusCode)) { if (statusCode >= 100 && statusCode <= 599) { return res.sendStatus(statusCode); } return res.sendStatus(500); } if (this.config.unwantedVisitorAction === 2) { res.send(`<iframe src="${this.config.unwantedVisitorTo}" width="100%" height="100%" align="left"></iframe> <style>body { padding: 0; margin: 0; } iframe { margin: 0; padding: 0; border: 0; }</style>`); } else if (this.config.unwantedVisitorAction === 3) { this.httpRequest(new url_1.URL(this.config.unwantedVisitorTo), { method: 'GET' }) .then(content => res.send(content)) .catch(fetchError => { console.error(`Fetching unwanted content error: ${fetchError.message}`); res.status(500).send('Error fetching unwanted content.'); }); } else { res.redirect(302, this.config.unwantedVisitorTo); } } else { res.status(403).send(` <!DOCTYPE html> <html lang="en"> <head> <title>Access Denied</title> <style>.sep { border-bottom: 5px black dotted; }</style> </head> <body> <div><b>Access Denied!</b></div> </body> </html> `); } } /** * Returns content for blocked visitors based on the configured action. * @returns {Promise<string>} The response content for blocked visitors. */ getBlockedContent() { return __awaiter(this, void 0, void 0, function* () { if (this.config.unwantedVisitorTo) { const statusCode = Number(this.config.unwantedVisitorTo); if (!isNaN(statusCode)) { if (statusCode >= 100 && statusCode <= 599) { return statusCode; } return 500; } if (this.config.unwantedVisitorAction === 2) { // Return an iframe with the URL return `<iframe src="${this.config.unwantedVisitorTo}" width="100%" height="100%" align="left"></iframe> <style>body { padding: 0; margin: 0; } iframe { margin: 0; padding: 0; border: 0; }</style>`; } else if (this.config.unwantedVisitorAction === 3) { // Return the content fetched from the URL try { return yield this.httpRequest(new url_1.URL(this.config.unwantedVisitorTo), { method: 'GET' }); } catch (error) { console.error('Error fetching content:', error); return '<p>Content not available</p>'; // Fallback content in case of error } } else { // Return HTML with JavaScript redirection return ` <p>Redirecting to <a href="${this.config.unwantedVisitorTo}">${this.config.unwantedVisitorTo}</a></p> <script> setTimeout(function() { window.location.href = "${this.config.unwantedVisitorTo}"; }, 1000); </script>`; } } // Return an HTML access denied message return '<p>Access Denied!</p>'; }); } /** * Makes an HTTPS request. * @param url - The URL to request. * @param options - The options for the request. * @returns {Promise<string>} The response body. */ httpRequest(url, options) { return new Promise((resolve, reject) => { const req = https.request(url, options, (res) => { let data = ''; res.on('data', (chunk) => { data += chunk; }); res.on('end', () => { resolve(data); }); }); req.on('error', (e) => { reject(e); }); req.end(); }); } /** * Validates if an IP address is valid. * Uses the `net` module to check if the IP address is a valid IPv4 or IPv6 address. * * @param {string} ip - The IP address to validate. * @returns {boolean} True if the IP address is valid, false otherwise. */ isValidIp(ip) { return net.isIPv4(ip) || net.isIPv6(ip); } } exports.VisitorTrafficFiltering = VisitorTrafficFiltering;