mirror of
https://github.com/Crazyco-xyz/48hr.email.git
synced 2026-01-11 11:49:35 +01:00
[Feat]: Add bot detection note
Just use my API, smh
This commit is contained in:
parent
a48ae65885
commit
b2433c29e3
7 changed files with 448 additions and 4 deletions
279
infrastructure/web/middleware/bot-detect.js
Normal file
279
infrastructure/web/middleware/bot-detect.js
Normal file
|
|
@ -0,0 +1,279 @@
|
|||
// Exhaustive bot detection middleware for Express
|
||||
// Flags likely bots and sets res.locals.suspectedBot
|
||||
// Uses multiple signals: User-Agent, Accept, Referer, Accept-Language, cookies, IP, and request rate
|
||||
|
||||
const knownBotUserAgents = [
|
||||
/HeadlessChrome/i,
|
||||
/PhantomJS/i,
|
||||
/Puppeteer/i,
|
||||
/node\.js/i,
|
||||
/curl/i,
|
||||
/wget/i,
|
||||
/python/i,
|
||||
/Go-http-client/i,
|
||||
/Java\//i,
|
||||
/libwww-perl/i,
|
||||
/scrapy/i,
|
||||
/httpclient/i,
|
||||
/http_request2/i,
|
||||
/lwp::simple/i,
|
||||
/okhttp/i,
|
||||
/mechanize/i,
|
||||
/axios/i,
|
||||
/rest-client/i,
|
||||
/httpie/i,
|
||||
/powershell/i,
|
||||
/http.rb/i,
|
||||
/fetch/i,
|
||||
/httpclient/i,
|
||||
/spider/i,
|
||||
/bot/i,
|
||||
/spider/i,
|
||||
/crawler/i,
|
||||
/slurp/i,
|
||||
/bingbot/i,
|
||||
/yandex/i,
|
||||
/duckduckgo/i,
|
||||
/baiduspider/i,
|
||||
/sogou/i,
|
||||
/exabot/i,
|
||||
/facebot/i,
|
||||
/ia_archiver/i,
|
||||
/Google-Read-Aloud/i, // Google Read Aloud
|
||||
/Google-Structured-Data-Testing-Tool/i,
|
||||
/Google-PageRenderer/i,
|
||||
/Google Favicon/i,
|
||||
/Googlebot/i,
|
||||
/AdsBot-Google/i,
|
||||
/Feedfetcher-Google/i,
|
||||
/APIs-Google/i,
|
||||
/bingpreview/i,
|
||||
/facebookexternalhit/i,
|
||||
/WhatsApp/i,
|
||||
/TelegramBot/i,
|
||||
/Slackbot/i,
|
||||
/Discordbot/i,
|
||||
/Applebot/i,
|
||||
/DuckDuckBot/i,
|
||||
/embedly/i,
|
||||
/LinkedInBot/i,
|
||||
/outbrain/i,
|
||||
/pinterest/i,
|
||||
/quora link preview/i,
|
||||
/rogerbot/i,
|
||||
/showyoubot/i,
|
||||
/SkypeUriPreview/i,
|
||||
/Slack-ImgProxy/i,
|
||||
/Twitterbot/i,
|
||||
/vkShare/i,
|
||||
/W3C_Validator/i,
|
||||
/redditbot/i,
|
||||
/FlipboardProxy/i,
|
||||
/Qwantify/i,
|
||||
/SEMrushBot/i,
|
||||
/AhrefsBot/i,
|
||||
/MJ12bot/i,
|
||||
/DotBot/i,
|
||||
/BLEXBot/i,
|
||||
/YandexBot/i,
|
||||
/Screaming Frog/i,
|
||||
/SiteAuditBot/i,
|
||||
/UptimeRobot/i,
|
||||
/Pingdom/i,
|
||||
/StatusCake/i,
|
||||
/ZoominfoBot/i,
|
||||
/Google-Safety/i,
|
||||
/Lighthouse/i,
|
||||
/Accessibility/i,
|
||||
/NVDA/i,
|
||||
/JAWS/i,
|
||||
/VoiceOver/i,
|
||||
/ScreenReader/i,
|
||||
/axe-core/i,
|
||||
/pa11y/i,
|
||||
/waveapi/i,
|
||||
/tenon/i,
|
||||
/Siteimprove/i,
|
||||
/SiteAnalyzer/i,
|
||||
/Sitebulb/i,
|
||||
/SEO PowerSuite/i,
|
||||
/SEOsitecheckup/i,
|
||||
/SEO Crawler/i,
|
||||
/SEO-Checker/i,
|
||||
/SEO-Tool/i,
|
||||
/SEO-Analyzer/i,
|
||||
/SEO-Tester/i,
|
||||
/SEO-SpyGlass/i,
|
||||
/SEO-Toolkit/i,
|
||||
/SEO-Tools/i,
|
||||
/SEO-Profiler/i,
|
||||
/SEO-Checker/i,
|
||||
/SEO-Tool/i,
|
||||
/SEO-Analyzer/i,
|
||||
/SEO-Tester/i,
|
||||
/SEO-SpyGlass/i,
|
||||
/SEO-Toolkit/i,
|
||||
/SEO-Tools/i,
|
||||
/SEO-Profiler/i
|
||||
];
|
||||
|
||||
const knownHeadlessIndicators = [
|
||||
'Headless',
|
||||
'PhantomJS',
|
||||
'Puppeteer',
|
||||
'Selenium',
|
||||
'Nightmare',
|
||||
'SlimerJS',
|
||||
'Zombie',
|
||||
'CasperJS',
|
||||
'TrifleJS',
|
||||
'HtmlUnit',
|
||||
'Splash',
|
||||
'Playwright'
|
||||
];
|
||||
|
||||
// Additional bypass and automation checks
|
||||
function hasSuspiciousHeaders(req) {
|
||||
// Some automation tools set these headers
|
||||
if (req.get('X-Requested-With') && req.get('X-Requested-With').toLowerCase() !== 'xmlhttprequest') return true;
|
||||
if (req.get('X-Purpose')) return true;
|
||||
if (req.get('X-Moz')) return true;
|
||||
if (req.get('X-ATT-DeviceId')) return true;
|
||||
if (req.get('X-Wap-Profile')) return true;
|
||||
if (req.get('X-OperaMini-Phone-UA')) return true;
|
||||
if (req.get('X-OperaMini-Features')) return true;
|
||||
if (req.get('X-Device-User-Agent')) return true;
|
||||
if (req.get('X-Original-User-Agent')) return true;
|
||||
if (req.get('X-Device-Id')) return true;
|
||||
if (req.get('X-Forwarded-For') && req.get('X-Forwarded-For').split(',').length > 3) return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
// In-memory request rate tracking (per IP)
|
||||
const requestLog = {};
|
||||
const RATE_WINDOW_MS = 10 * 1000; // 10 seconds
|
||||
const MAX_REQUESTS_PER_WINDOW = 30;
|
||||
|
||||
function isRapidRequester(ip) {
|
||||
const now = Date.now();
|
||||
if (!requestLog[ip]) requestLog[ip] = [];
|
||||
// Remove old entries
|
||||
requestLog[ip] = requestLog[ip].filter(ts => now - ts < RATE_WINDOW_MS);
|
||||
requestLog[ip].push(now);
|
||||
return requestLog[ip].length > MAX_REQUESTS_PER_WINDOW;
|
||||
}
|
||||
|
||||
module.exports = function botDetect(req, res, next) {
|
||||
// If suppression cookie is set, skip detection
|
||||
if (req.cookies && req.cookies.bot_check_passed) {
|
||||
res.locals.suspectedBot = false;
|
||||
return next();
|
||||
}
|
||||
|
||||
let score = 0;
|
||||
const reasons = [];
|
||||
|
||||
// Header and request info (declare all before use)
|
||||
const ua = req.get('User-Agent') || '';
|
||||
const accept = req.get('Accept') || '';
|
||||
const referer = req.get('Referer') || '';
|
||||
const acceptLang = req.get('Accept-Language') || '';
|
||||
const hasCookies = !!req.headers.cookie;
|
||||
const ip = req.ip || req.connection.remoteAddress;
|
||||
const path = req.path || '';
|
||||
|
||||
// Check for suspicious/bypass headers
|
||||
if (hasSuspiciousHeaders(req)) {
|
||||
score += 2;
|
||||
reasons.push('Suspicious/bypass headers');
|
||||
}
|
||||
|
||||
// Google Read Aloud and similar tools: look for Accept header with 'application/ssml+xml' or 'text/speech'
|
||||
if (accept.includes('ssml+xml') || accept.includes('text/speech')) {
|
||||
score += 2;
|
||||
reasons.push('Speech synthesis Accept header');
|
||||
}
|
||||
|
||||
// Accessibility Accept headers (screen readers, etc)
|
||||
if (accept.includes('application/x-nvda') || accept.includes('application/x-jaws')) {
|
||||
score += 1;
|
||||
reasons.push('Accessibility Accept header');
|
||||
}
|
||||
|
||||
// Check for automation framework cookies (common for Selenium, Puppeteer, etc)
|
||||
if (req.headers.cookie && (req.headers.cookie.includes('puppeteer') || req.headers.cookie.includes('selenium'))) {
|
||||
score += 2;
|
||||
reasons.push('Automation framework cookie');
|
||||
}
|
||||
|
||||
// User-Agent checks
|
||||
if (!ua) {
|
||||
score += 2;
|
||||
reasons.push('Missing User-Agent');
|
||||
} else {
|
||||
if (knownBotUserAgents.some(pat => pat.test(ua))) {
|
||||
score += 3;
|
||||
reasons.push('Known bot User-Agent');
|
||||
}
|
||||
if (knownHeadlessIndicators.some(ind => ua.includes(ind))) {
|
||||
score += 2;
|
||||
reasons.push('Headless browser indicator');
|
||||
}
|
||||
if (ua.length < 10) {
|
||||
score += 1;
|
||||
reasons.push('Suspiciously short User-Agent');
|
||||
}
|
||||
}
|
||||
|
||||
// Accept header
|
||||
if (!accept || accept === '*/*') {
|
||||
score += 1;
|
||||
reasons.push('Suspicious Accept header');
|
||||
}
|
||||
|
||||
// Referer
|
||||
if (!referer && req.method === 'POST') {
|
||||
score += 1;
|
||||
reasons.push('Missing Referer on POST');
|
||||
}
|
||||
|
||||
// Accept-Language
|
||||
if (!acceptLang) {
|
||||
score += 1;
|
||||
reasons.push('Missing Accept-Language');
|
||||
}
|
||||
|
||||
// Cookies
|
||||
if (!hasCookies) {
|
||||
score += 1;
|
||||
reasons.push('No cookies sent');
|
||||
}
|
||||
|
||||
// IP checks (basic, not using blocklists)
|
||||
if (isRapidRequester(ip)) {
|
||||
score += 2;
|
||||
reasons.push('Rapid request rate');
|
||||
}
|
||||
|
||||
// HTTP method
|
||||
if (req.method && !['GET', 'POST', 'HEAD'].includes(req.method)) {
|
||||
score += 1;
|
||||
reasons.push('Unusual HTTP method');
|
||||
}
|
||||
|
||||
// Path checks (bots often hit /robots.txt, /admin, etc)
|
||||
if (['/robots.txt', '/admin', '/wp-login.php', '/xmlrpc.php'].includes(path)) {
|
||||
score += 2;
|
||||
reasons.push('Bot-targeted path');
|
||||
}
|
||||
|
||||
// If score is high, flag as bot
|
||||
const threshold = 3;
|
||||
if (score >= threshold) {
|
||||
res.locals.suspectedBot = true;
|
||||
res.locals.botDetectionReasons = reasons;
|
||||
} else {
|
||||
res.locals.suspectedBot = false;
|
||||
}
|
||||
next();
|
||||
}
|
||||
|
Before Width: | Height: | Size: 46 KiB After Width: | Height: | Size: 46 KiB |
|
|
@ -1,3 +1,71 @@
|
|||
/* Bot detection popup */
|
||||
|
||||
.bot-detect-popup-overlay {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
background: var(--overlay-purple-50, rgba(155, 77, 202, 0.5));
|
||||
z-index: 99999;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transition: background 0.2s;
|
||||
}
|
||||
|
||||
.bot-detect-popup-modal {
|
||||
background: var(--color-bg-medium, #2F2B36);
|
||||
color: var(--color-text-primary, #cccccc);
|
||||
padding: 2em 2.5em;
|
||||
border-radius: 14px;
|
||||
max-width: 420px;
|
||||
width: 90vw;
|
||||
text-align: center;
|
||||
box-shadow: 0 2px 24px var(--overlay-black-45, rgba(0, 0, 0, 0.45));
|
||||
font-size: 1.1em;
|
||||
border: 1.5px solid var(--color-accent-purple, #9b4dca);
|
||||
}
|
||||
|
||||
.bot-detect-popup-modal h2 {
|
||||
margin-top: 0;
|
||||
color: var(--color-accent-purple, #9b4dca);
|
||||
font-weight: 700;
|
||||
font-size: 1.6em;
|
||||
}
|
||||
|
||||
.bot-detect-popup-modal a {
|
||||
color: var(--color-accent-purple-bright, #6c5ce7);
|
||||
text-decoration: underline;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.bot-detect-popup-modal a:hover {
|
||||
color: var(--color-accent-orange, #ca5414);
|
||||
}
|
||||
|
||||
.bot-detect-popup-modal button {
|
||||
margin-top: 1.5em;
|
||||
font-size: 1em;
|
||||
border-radius: 6px;
|
||||
background: var(--color-accent-purple, #9b4dca);
|
||||
color: #fff;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
font-weight: 600;
|
||||
box-shadow: 0 1px 4px var(--overlay-black-30, rgba(0, 0, 0, 0.3));
|
||||
transition: background 0.15s;
|
||||
}
|
||||
|
||||
.bot-detect-popup-modal button:hover {
|
||||
background: var(--color-accent-purple-bright, #6c5ce7);
|
||||
}
|
||||
|
||||
.bot-popup-no-scroll {
|
||||
overflow: hidden !important;
|
||||
height: 100vh !important;
|
||||
}
|
||||
|
||||
:root {
|
||||
/* Base colors */
|
||||
--color-bg-dark: #131516;
|
||||
|
|
|
|||
78
infrastructure/web/views/bot-popup.twig
Normal file
78
infrastructure/web/views/bot-popup.twig
Normal file
|
|
@ -0,0 +1,78 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Automation Detected | 48hr.email</title>
|
||||
<link rel="icon" type="image/x-icon" href="/images/favicon.ico">
|
||||
<link rel="shortcut icon" href="/images/favicon.ico">
|
||||
<link rel="apple-touch-icon" href="/images/logo.png">
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, minimal-ui">
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
<title>Automation Detected | 48hr.email</title>
|
||||
<!-- SEO Meta Tags -->
|
||||
<meta name="description" content="Your temporary Inbox. Create instant throwaway email addresses to protect your privacy. No registration required. Emails auto-delete after 48 hours.">
|
||||
<meta name="keywords" content="temporary email, disposable email, throwaway email, fake email, temp mail, anonymous email, 48hr email, privacy protection, burner email">
|
||||
<meta name="author" content="CrazyCo">
|
||||
<meta name="robots" content="index, follow">
|
||||
<meta name="googlebot" content="index, follow">
|
||||
<link rel="canonical" href="https://48hr.email/">
|
||||
<!-- Open Graph / Facebook -->
|
||||
<meta property="og:type" content="website">
|
||||
<meta property="og:url" content="https://48hr.email/">
|
||||
<meta property="og:title" content="48hr.email - Your temporary Inbox">
|
||||
<meta property="og:description" content="Protect your privacy with free temporary email addresses. No registration required. Emails auto-delete after 48 hours.">
|
||||
<meta property="og:image" content="/images/logo.png">
|
||||
<meta property="og:site_name" content="48hr.email">
|
||||
<!-- Twitter Card -->
|
||||
<meta name="twitter:card" content="summary">
|
||||
<meta name="twitter:url" content="https://48hr.email/">
|
||||
<meta name="twitter:title" content="48hr.email - Your temporary Inbox">
|
||||
<meta name="twitter:description" content="Free temporary email service. Protect your privacy with disposable email addresses.">
|
||||
<meta name="twitter:image" content="/images/logo.png">
|
||||
<!-- Additional Meta Tags -->
|
||||
<meta name="theme-color" content="#9b4dca">
|
||||
<meta name="darkreader-lock">
|
||||
<meta name="mobile-web-app-capable" content="yes">
|
||||
<meta name="apple-mobile-web-app-capable" content="yes">
|
||||
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
|
||||
<meta name="apple-mobile-web-app-title" content="48hr.email">
|
||||
<!-- Favicon -->
|
||||
<link rel="icon" type="image/x-icon" href="/images/favicon.ico">
|
||||
<link rel="shortcut icon" href="/images/favicon.ico">
|
||||
<link rel="apple-touch-icon" href="/images/logo.png">
|
||||
<link rel="stylesheet" href="/stylesheets/custom.css">
|
||||
</head>
|
||||
<body class="bot-popup-no-scroll">
|
||||
<div class="bot-detect-popup-overlay" id="bot-detect-popup">
|
||||
<div class="bot-detect-popup-modal">
|
||||
<h2>Automation Detected</h2>
|
||||
<p>We have detected that you may be using automation or a bot to access this site.<br><br>
|
||||
If you want to automate things, please use our <a href="/api" target="_blank">official API</a>.<br><br>
|
||||
See the <a href="https://github.com/Crazyco-xyz/48hr.email/wiki" target="_blank"><b>API Documentation</b></a> for details.<br><br>
|
||||
To continue, please close this popup manually. If you think you're seeing this by mistake, let us know!</p>
|
||||
<button id="bot-popup-close">I Understand</button>
|
||||
</div>
|
||||
</div>
|
||||
<script>
|
||||
function setCookie(name, value, days) {
|
||||
var expires = '';
|
||||
if (days) {
|
||||
var date = new Date();
|
||||
date.setTime(date.getTime() + (days*24*60*60*1000));
|
||||
expires = '; expires=' + date.toUTCString();
|
||||
}
|
||||
document.cookie = name + '=' + (value || '') + expires + '; path=/';
|
||||
}
|
||||
document.getElementById('bot-popup-close').onclick = function() {
|
||||
setCookie('bot_check_passed', 'true', 7);
|
||||
window.location.reload();
|
||||
};
|
||||
// Prevent all interaction with the rest of the page
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
document.body.classList.add('bot-popup-no-scroll');
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -41,8 +41,8 @@
|
|||
<meta name="apple-mobile-web-app-title" content="48hr.email">
|
||||
|
||||
<!-- Favicon -->
|
||||
<link rel="icon" type="image/x-icon" href="/images/logo.ico">
|
||||
<link rel="shortcut icon" href="/images/logo.ico">
|
||||
<link rel="icon" type="image/x-icon" href="/images/favicon.ico">
|
||||
<link rel="shortcut icon" href="/images/favicon.ico">
|
||||
<link rel="apple-touch-icon" href="/images/logo.png">
|
||||
|
||||
<!-- Stylesheets -->
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
const botDetect = require('./middleware/bot-detect')
|
||||
const path = require('path')
|
||||
const http = require('http')
|
||||
const debug = require('debug')('48hr-email:server')
|
||||
|
|
@ -58,6 +59,24 @@ app.use(session({
|
|||
cookie: { maxAge: 24 * 60 * 60 * 1000 } // 24 hours
|
||||
}))
|
||||
|
||||
|
||||
|
||||
// Bot detection middleware (after cookies/session, before routes)
|
||||
app.use(botDetect)
|
||||
|
||||
// If bot detected and not suppressed, render only the popup page and halt further processing
|
||||
app.use((req, res, next) => {
|
||||
// Allow static assets (css, js, images, favicon, etc) and all /api/* routes even if bot detected
|
||||
if (res.locals.suspectedBot && !(req.cookies && req.cookies.bot_check_passed)) {
|
||||
const asset = req.path.match(/\.(css|js|png|jpg|jpeg|gif|svg|ico|woff2?|ttf|eot)$/i);
|
||||
if (asset) return next();
|
||||
if (req.path.startsWith('/api/')) return next();
|
||||
// For non-asset, non-API requests, render only the popup page
|
||||
return res.status(200).render('bot-popup');
|
||||
}
|
||||
next();
|
||||
});
|
||||
|
||||
// Clear lock session data when user goes Home (but preserve authentication)
|
||||
app.get('/', (req, res, next) => {
|
||||
if (req.session && req.session.lockedInbox) {
|
||||
|
|
@ -243,4 +262,4 @@ server.on('listening', () => {
|
|||
server.emit('ready')
|
||||
})
|
||||
|
||||
module.exports = { app, io, server }
|
||||
module.exports = { app, io, server }
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "48hr.email",
|
||||
"version": "2.3.2",
|
||||
"version": "2.3.3",
|
||||
"private": false,
|
||||
"description": "48hr.email is your favorite open-source tempmail client.",
|
||||
"keywords": [
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue