Merge pull request #6 from saschaleib/captcha

Captcha merge
This commit is contained in:
Sascha Leib
2025-12-06 19:05:57 +01:00
committed by GitHub
31 changed files with 12886 additions and 352 deletions

3
.gitignore vendored
View File

@@ -2,5 +2,6 @@
logs/*.log.txt
logs/*.srv.txt
logs/*.tck.txt
config/user-config.json
logs/*.captcha.txt
config/user-*.*
php_errors.log

View File

@@ -1,10 +1,8 @@
# DokuWiki Bot Monitoring Plugin
Plugin for live-monitoring your DokuWiki instance for bot activity
IMPORTANT: This is an experimental plugin to investigate bot traffic. This is not a "install and forget" software, but rather requires that you actively look at and manage the log files.
IMPORTANT: This is an experimental plugin to investigate bot traffic. This is not a "install and forget" software, but rather requires that you actively look at and manage data for this to be useful.
This plugin creates various log files in its own "logs" directory. These files can get quite large, and you should check them actively.
In addition to collecting a lot fo information about bot activity on your server, it now also has a simple Captcha function that you can use to block off bots from downloading your precious content. It is however advisable to only activate this after you already have a better understanding of your own site's traffic patterns (both by bots and by humans) to avoid over-blocking legitimate users.
Also, these files can get quite large and fill up your server. Make sure to manually delete older files from time to time!
For more information, please see the DokuWiki Plugin page at: https://www.dokuwiki.org/plugin:botmon
For more information, please see the DokuWiki Plugin page at: https://www.dokuwiki.org/plugin:botmon and the documentation found at: https://leib.be/sascha/projects/dokuwiki/botmon/index

View File

@@ -13,6 +13,26 @@ use dokuwiki\Logger;
class action_plugin_botmon extends DokuWiki_Action_Plugin {
public function __construct() {
// determine if a captcha should be loaded:
$this->showCaptcha = 'Z'; // Captcha unknown
$useCaptcha = $this->getConf('useCaptcha'); // should we show a captcha?
if ($useCaptcha !== 'disabled') {
if ($_SERVER['REQUEST_METHOD'] == 'HEAD') {
$this->showCaptcha = 'H'; // Method is HEAD, no need for captcha
} elseif ($this->captchaWhitelisted()) {
$this->showCaptcha = 'W'; // IP is whitelisted, no captcha
} elseif ($this->hasCaptchaCookie()) {
$this->showCaptcha = 'N'; // No, user already has a cookie, don't show the captcha
} else {
$this->showCaptcha = 'Y'; // Yes, show the captcha
}
}
}
/**
* Registers a callback functions
*
@@ -23,13 +43,23 @@ class action_plugin_botmon extends DokuWiki_Action_Plugin {
global $ACT;
// populate the session id and type:
$this->setSessionInfo();
// insert header data into the page:
if ($ACT == 'show') {
if ($ACT == 'show' || $ACT == 'edit' || $ACT == 'media') {
$controller->register_hook('TPL_METAHEADER_OUTPUT', 'BEFORE', $this, 'insertHeader');
// Override the page rendering, if a captcha needs to be displayed:
$controller->register_hook('TPL_ACT_RENDER', 'BEFORE', $this, 'insertCaptchaCode');
} else if ($ACT == 'admin' && isset($_REQUEST['page']) && $_REQUEST['page'] == 'botmon') {
$controller->register_hook('TPL_METAHEADER_OUTPUT', 'BEFORE', $this, 'insertAdminHeader');
}
}
// also show a captcha before the image preview
$controller->register_hook('TPL_IMG_DISPLAY', 'BEFORE', $this, 'showImageCaptcha');
// write to the log after the page content was displayed:
$controller->register_hook('TPL_CONTENT_DISPLAY', 'AFTER', $this, 'writeServerLog');
@@ -38,7 +68,7 @@ class action_plugin_botmon extends DokuWiki_Action_Plugin {
/* session information */
private $sessionId = null;
private $sessionType = '';
private $ipAddress = null;
private $showCaptcha = 'X';
/**
* Inserts tracking code to the page header
@@ -51,17 +81,9 @@ class action_plugin_botmon extends DokuWiki_Action_Plugin {
global $INFO;
// populate the session id and type:
$this->getSessionInfo();
// is there a user logged in?
$username = ( !empty($INFO['userinfo']) && !empty($INFO['userinfo']['name']) ? $INFO['userinfo']['name'] : '');
// build the tracker code:
$code = "document._botmon = {'t0': Date.now(), 'session': '" . json_encode($this->sessionId) . "'};" . NL;
if ($username) {
$code .= DOKU_TAB . DOKU_TAB . 'document._botmon.user = "' . $username . '";'. NL;
}
$code = $this->getBMHeader();
// add the deferred script loader::
$code .= DOKU_TAB . DOKU_TAB . "addEventListener('DOMContentLoaded', function(){" . NL;
@@ -70,10 +92,25 @@ class action_plugin_botmon extends DokuWiki_Action_Plugin {
$code .= DOKU_TAB . DOKU_TAB . DOKU_TAB . "e.src='".DOKU_BASE."lib/plugins/botmon/client.js';" . NL;
$code .= DOKU_TAB . DOKU_TAB . DOKU_TAB . "document.getElementsByTagName('head')[0].appendChild(e);" . NL;
$code .= DOKU_TAB . DOKU_TAB . "});";
$event->data['script'][] = ['_data' => $code];
}
/* create the BM object code for insertion into a script element: */
private function getBMHeader() {
// build the tracker code:
$code = DOKU_TAB . DOKU_TAB . "document._botmon = {t0: Date.now(), session: " . json_encode($this->sessionId) . ", seed: " . json_encode($this->getConf('captchaSeed')) . ", ip: " . json_encode($_SERVER['REMOTE_ADDR']) . "};" . NL;
// is there a user logged in?
$username = ( !empty($INFO['userinfo']) && !empty($INFO['userinfo']['name']) ? $INFO['userinfo']['name'] : '');
if ($username) {
$code .= DOKU_TAB . DOKU_TAB . 'document._botmon.user = "' . $username . '";'. NL;
}
return $code;
}
/**
* Inserts tracking code to the page header
* (only called on 'show' actions)
@@ -87,7 +124,6 @@ class action_plugin_botmon extends DokuWiki_Action_Plugin {
$event->data['script'][] = ['src' => DOKU_BASE.'lib/plugins/botmon/admin.js', 'defer' => 'defer', '_data' => ''];
}
/**
* Writes data to the server log.
*
@@ -107,7 +143,7 @@ class action_plugin_botmon extends DokuWiki_Action_Plugin {
// create the log array:
$logArr = Array(
$_SERVER['REMOTE_ADDR'], /*$this->ipAddress, // remote IP */
$_SERVER['REMOTE_ADDR'], /* remote IP */
$pageId, /* page ID */
$this->sessionId, /* Session ID */
$this->sessionType, /* session ID type */
@@ -116,7 +152,9 @@ class action_plugin_botmon extends DokuWiki_Action_Plugin {
$_SERVER['HTTP_REFERER'] ?? '', /* HTTP Referrer */
substr($conf['lang'],0,2), /* page language */
implode(',', array_unique(array_map( function($it) { return substr(trim($it),0,2); }, explode(',',trim($_SERVER['HTTP_ACCEPT_LANGUAGE'], " \t;,*"))))), /* accepted client languages */
$this->getCountryCode() /* GeoIP country code */
$this->getCountryCode(), /* GeoIP country code */
$this->showCaptcha, /* show captcha? */
$_SERVER['REQUEST_METHOD'] ?? '' /* request method */
);
//* create the log line */
@@ -140,7 +178,7 @@ class action_plugin_botmon extends DokuWiki_Action_Plugin {
private function getCountryCode() {
$country = ( $this->ipAddress == 'localhost' ? 'local' : 'ZZ' ); // default if no geoip is available!
$country = ( $_SERVER['REMOTE_ADDR'] == '127.0.0.1' ? 'local' : 'ZZ' ); // default if no geoip is available!
$lib = $this->getConf('geoiplib'); /* which library to use? (can only be phpgeoip or disabled) */
@@ -158,10 +196,7 @@ class action_plugin_botmon extends DokuWiki_Action_Plugin {
return $country;
}
private function getSessionInfo() {
$this->ipAddress = $_SERVER['REMOTE_ADDR'] ?? null;
if ($this->ipAddress == '127.0.0.1' || $this->ipAddress == '::1') $this->ipAddress = 'localhost';
private function setSessionInfo() {
// what is the session identifier?
if (isset($_SESSION)) {
@@ -178,13 +213,373 @@ class action_plugin_botmon extends DokuWiki_Action_Plugin {
$this->sessionId = session_id();
$this->sessionType = 'php';
}
if (!$this->sessionId && $this->ipAddress) { /* no PHP session ID, try IP address */
$this->sessionId = $this->ipAddress;
if (!$this->sessionId) { /* no PHP session ID, try IP address */
$this->sessionId = $_SERVER['REMOTE_ADDR'];
$this->sessionType = 'ip';
}
if (!$this->sessionId) { /* if everything else fails, just us a random ID */
$this->sessionId = rand(1000000, 9999999);
$this->sessionType = 'rand';
if (!$this->sessionId) { /* if all fails, use random data */
$this->sessionId = rand(100000000, 999999999);
$this->sessionType = 'rnd';
}
}
public function insertCaptchaCode(Event $event) {
$useCaptcha = $this->getConf('useCaptcha'); // which background to show?
// only if we previously determined that we need a captcha:
if ($this->showCaptcha == 'Y') {
echo '<h1 class="sectionedit1">'; tpl_pagetitle(); echo "</h1>\n"; // always show the original page title
$event->preventDefault(); // don't show normal content
switch ($useCaptcha) {
case 'loremipsum':
$this->insertLoremIpsum(); // show dada filler instead of text
break;
case 'dada':
$this->insertDadaFiller(); // show dada filler instead of text
break;
}
// insert the captcha loader code:
echo '<script>' . NL;
// add the deferred script loader::
echo DOKU_TAB . "addEventListener('DOMContentLoaded', function(){" . NL;
echo DOKU_TAB . DOKU_TAB . "const cj=document.createElement('script');" . NL;
echo DOKU_TAB . DOKU_TAB . "cj.async=true;cj.defer=true;cj.type='text/javascript';" . NL;
echo DOKU_TAB . DOKU_TAB . "cj.src='".DOKU_BASE."lib/plugins/botmon/captcha.js';" . NL;
echo DOKU_TAB . DOKU_TAB . "document.getElementsByTagName('head')[0].appendChild(cj);" . NL;
echo DOKU_TAB . "});" . NL;
// add the translated strings for the captcha:
echo DOKU_TAB . '$BMLocales = {' . NL;
echo DOKU_TAB . DOKU_TAB . '"dlgTitle": ' . json_encode($this->getLang('bm_dlgTitle')) . ',' . NL;
echo DOKU_TAB . DOKU_TAB . '"dlgSubtitle": ' . json_encode($this->getLang('bm_dlgSubtitle')) . ',' . NL;
echo DOKU_TAB . DOKU_TAB . '"dlgConfirm": ' . json_encode($this->getLang('bm_dlgConfirm')) . ',' . NL;
echo DOKU_TAB . DOKU_TAB . '"dlgChecking": ' . json_encode($this->getLang('bm_dlgChecking')) . ',' . NL;
echo DOKU_TAB . DOKU_TAB . '"dlgLoading": ' . json_encode($this->getLang('bm_dlgLoading')) . ',' . NL;
echo DOKU_TAB . DOKU_TAB . '"dlgError": ' . json_encode($this->getLang('bm_dlgError')) . ',' . NL;
echo DOKU_TAB . '};' . NL;
// captcha configuration options
echo DOKU_TAB . '$BMConfig = {' . NL;
echo DOKU_TAB . DOKU_TAB . '"captchaBypass": ' . json_encode($this->getConf('captchaBypass')) . NL;
echo DOKU_TAB . '};' . NL;
echo '</script>' . NL;
// insert a warning message for users without JavaScript:
echo '<dialog open closedby="any" id="BM__NoJSWarning"><p>' . $this->getLang('bm_noJsWarning') . '</p></dialog>' . NL;
}
}
public function showImageCaptcha(Event $event, $param) {
$useCaptcha = $this->getConf('useCaptcha');
echo '<script>' . $this->getBMHeader($event, $param) . '</script>';
$cCode = '-';
if ($useCaptcha !== 'disabled') {
if ($this->captchaWhitelisted()) {
$cCode = 'W'; // whitelisted
}
elseif ($this->hasCaptchaCookie()) {
$cCode = 'N'; // user already has a cookie
}
else {
$cCode = 'Y'; // show the captcha
echo '<svg width="100%" height="100%" viewBox="0 0 800 400" version="1.1" xmlns="http://www.w3.org/2000/svg"><path d="M1,1l798,398" style="fill:none;stroke:#f00;stroke-width:1px;"/><path d="M1,399l798,-398" style="fill:none;stroke:#f00;stroke-width:1px;"/><rect x="1" y="1" width="798" height="398" style="fill:none;stroke:#000;stroke-width:1px;"/></svg>'; // placeholder image
$event->preventDefault(); // don't show normal content
// TODO Insert dummy image
$this->insertCaptchaLoader(); // and load the captcha
}
};
$this->showCaptcha = $cCode; // store the captcha code for the logfile
}
/**
* Checks if the user has a valid captcha cookie.
*
* @return boolean
* @access private
*
**/
private function hasCaptchaCookie() {
$cookieVal = isset($_COOKIE['DWConfirm']) ? $_COOKIE['DWConfirm'] : null;
$today = substr((new DateTime())->format('c'), 0, 10);
$raw = $this->getConf('captchaSeed') . ';' . $_SERVER['SERVER_NAME'] . ';' . $_SERVER['REMOTE_ADDR'] . ';' . $today;
$expected = hash('sha256', $raw);
// for debugging: write captcha data to the log:
$this->writeCaptchaLog($_SERVER['REMOTE_ADDR'], $cookieVal, $_SERVER['SERVER_NAME'], $expected);
return $cookieVal == $expected;
}
/**
* Writes data to the captcha log.
*
* @return void
*/
private function writeCaptchaLog($remote_addr, $cookieVal, $serverName, $expected) {
global $INFO;
$logArr = Array(
$remote_addr, /* remote IP */
$cookieVal, /* cookie value */
$this->getConf('captchaSeed'), /* seed */
$serverName, /* server name */
$expected, /* expected cookie value */
($cookieVal == $expected ? 'MATCH' : 'WRONG'), /* cookie matches expected value? */
$_SERVER['REQUEST_URI'] /* request URI */
);
//* create the log line */
$filename = __DIR__ .'/logs/' . gmdate('Y-m-d') . '.captcha.txt'; /* use GMT date for filename */
$logline = gmdate('Y-m-d H:i:s'); /* use GMT time for log entries */
foreach ($logArr as $tab) {
$logline .= "\t" . $tab;
};
/* write the log line to the file */
$logfile = fopen($filename, 'a');
if (!$logfile) die();
if (fwrite($logfile, $logline . "\n") === false) {
fclose($logfile);
die();
}
// in case of errors, write the cookie data to the log:
if (!$cookieVal) {
$logline = print_r($_COOKIE, true);
if (fwrite($logfile, $logline . "\n") === false) {
fclose($logfile);
die();
}
}
/* Done. close the file. */
fclose($logfile);
}
// check if the visitor's IP is on a whitelist:
private function captchaWhitelisted() {
// normalise IP address:
$ip = inet_pton($_SERVER['REMOTE_ADDR']);
// find which file to open:
$prefixes = ['user', 'default'];
foreach ($prefixes as $pre) {
$filename = __DIR__ .'/config/' . $pre . '-whitelist.txt';
if (file_exists($filename)) {
break;
}
}
if (file_exists($filename)) {
$lines = file($filename, FILE_SKIP_EMPTY_LINES);
foreach ($lines as $line) {
if (trim($line) !== '' && !str_starts_with($line, '#')) {
$col = explode("\t", $line);
if (count($col) >= 2) {
$from = inet_pton($col[0]);
$to = inet_pton($col[1]);
if ($ip >= $from && $ip <= $to) {
return true; /* IP whitelisted */
}
}
}
}
}
return false; /* IP not found in whitelist */
}
// inserts a blank box to ensure there is enough space for the captcha:
private function insertLoremIpsum() {
echo '<div class="level1">' . NL;
echo '<p>' . NL . 'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.'. NL . '</p>' . NL;
echo '<p>' . NL . 'At vero eos et accusamus et iusto odio dignissimos ducimus, qui blanditiis praesentium voluptatum deleniti atque corrupti, quos dolores et quas molestias excepturi sint, obcaecati cupiditate non provident, similique sunt in culpa, qui officia deserunt mollitia animi, id est laborum et dolorum fuga.'. NL . '</p>' . NL;
echo '</div>' . NL;
}
/* Generates a few paragraphs of Dada text to show instead of the article content */
private function insertDadaFiller() {
global $conf;
global $TOC;
global $ID;
// list of languages to search for the wordlist
$langs = array_unique([$conf['lang'], 'la']);
// find path to the first available wordlist:
foreach ($langs as $lang) {
$filename = __DIR__ .'/lang/' . $lang . '/wordlist.txt'; /* language-specific wordlist */
if (file_exists($filename)) {
break;
}
}
// load the wordlist file:
if (file_exists($filename)) {
$words = array();
$totalWeight = 0;
$lines = file($filename, FILE_SKIP_EMPTY_LINES);
foreach ($lines as $line) {
$arr = explode("\t", $line);
$arr[1] = ( count($arr) > 1 ? (int) trim($arr[1]) : 1 );
$totalWeight += (int) $arr[1];
array_push($words, $arr);
}
} else {
echo '<script> console.log("Cant generate filler text: wordlist file not found!"); </script>';
return;
}
// If a TOC exists, use it for the headlines:
if(is_array($TOC)) {
$toc = $TOC;
} else {
$meta = p_get_metadata($ID, '', METADATA_RENDER_USING_CACHE);
//$tocok = (isset($meta['internal']['toc']) ? $meta['internal']['toc'] : $tocok = true);
$toc = isset($meta['description']['tableofcontents']) ? $meta['description']['tableofcontents'] : null;
}
if (!$toc) { // no TOC, generate my own:
$hlCount = mt_rand(0, (int) $conf['tocminheads']);
$toc = array();
for ($i=0; $i<$hlCount; $i++) {
array_push($toc, $this->dadaMakeHeadline($words, $totalWeight)); // $toc
}
}
// if H1 heading is not in the TOC, add a chappeau section:
$chapeauCount = mt_rand(1, 3);
if ((int) $conf['toptoclevel'] > 1) {
echo "<div class=\"level1\">\n";
for ($i=0; $i<$chapeauCount; $i++) {
echo $this->dadaMakeParagraph($words, $totalWeight);
}
echo "</div>\n";
}
// text sections for each sub-headline:
foreach ($toc as $hl) {
echo $this->dadaMakeSection($words, $totalWeight, $hl);
}
}
private function dadaMakeSection($words, $totalWeight, $hl) {
global $conf;
// how many paragraphs?
$paragraphCount = mt_rand(1, 4);
// section level
$topTocLevel = (int) $conf['toptoclevel'];
$secLevel = $hl['level'] + 1;;
// return value:
$sec = "";
// make a headline:
if ($topTocLevel > 1 || $secLevel > 1) {
$sec .= "<h{$secLevel} id=\"{$hl['hid']}\">{$hl['title']}</h{$secLevel}>\n";
}
// add the paragraphs:
$sec .= "<div class=\"level{$secLevel}\">\n";
for ($i=0; $i<$paragraphCount; $i++) {
$sec .= $this->dadaMakeParagraph($words, $totalWeight);
}
$sec .= "</div>\n";
return $sec;
}
private function dadaMakeHeadline($words, $totalWeight) {
// how many words to generate?
$wordCount = mt_rand(2, 5);
// function returns an array:
$r = Array();
// generate the headline:
$hlArr = array();
for ($i=0; $i<$wordCount; $i++) {
array_push($hlArr, $this->dadaSelectRandomWord($words, $totalWeight));
}
$r['title'] = ucfirst(implode(' ', $hlArr));
$r['hid'] = preg_replace('/[^\w\d\-]+/i', '_', strtolower($r['title']));
$r['type'] = 'ul'; // always ul!
$r['level'] = 1; // always level 1 for now
return $r;
}
private function dadaMakeParagraph($words, $totalWeight) {
// how many words to generate?
$sentenceCount = mt_rand(2, 5);
$paragraph = array();
for ($i=0; $i<$sentenceCount; $i++) {
array_push($paragraph, $this->dadaMakeSentence($words, $totalWeight));
}
return "<p>\n" . implode(' ', $paragraph) . "\n</p>\n";
}
private function dadaMakeSentence($words, $totalWeight) {
// how many words to generate?
$wordCount = mt_rand(4, 20);
// generate the sentence:
$sentence = array();
for ($i=0; $i<$wordCount; $i++) {
array_push($sentence, $this->dadaSelectRandomWord($words, $totalWeight));
}
return ucfirst(implode(' ', $sentence)) . '.';
}
private function dadaSelectRandomWord($list, $totalWeight) {
// get a random selection:
$rand = mt_rand(0, $totalWeight);
// match the selection to the weighted list:
$cumulativeWeight = 0;
for ($i=0; $i<count($list); $i++) {
$cumulativeWeight += $list[$i][1];
if ($cumulativeWeight >= $rand) {
return $list[$i][0];
}
}
return '***';
}
}

162
admin.css
View File

@@ -7,24 +7,29 @@
/* icon items */
.has_icon {
display: inline-flex;
align-items: center;
width: auto;
white-space: nowrap;
}
.icon_only {
display: inline-grid;
grid-template-columns: 20px max-content;
overflow: hidden;
width: 20px;
width: 20px; max-width: 20px;
}
.has_icon, .icon_only {
& {
align-items: center;
column-gap: .25em;
overflow: hidden;
column-gap: .2em;
min-width: 20px;
}
&::before {
content: '';
display: inline-block;
width: 20px; height: 20px;
width: 20px; min-width: 20px; max-width: 20px;
height: 20px;
background: transparent none center no-repeat;
background-position: 0 0;
background-size: 20px;
@@ -106,8 +111,22 @@
&.cl_ecosia::before { background-position-y: -340px }
&.cl_webkit::before { background-position-y: -360px }
&.cl_operaold::before { background-position-y: -380px }
&.cl_wget::before { background-position-y: -400px }
&.cl_python::before { background-position-y: -420px }
&.cl_privacybrowser::before { background-position-y: -440px }
&.cl_other::before { background-image: url('img/more.svg') }
/* Captcha statuses */
&.captcha::before { background-image: url('img/captcha.png') }
&.cap_Y::before { background-position-y: -20px }
&.cap_N::before, &.cap_YN::before, &.cap_YYN::before { background-position-y: -40px }
&.cap_W::before { background-position-y: -60px }
&.cap_H::before { background-position-y: -80px }
&.cap_X::before, &.cap_undefined::before { background-position-y: -100px }
&.cap_YH::before, &.cap_YYH::before { background-position-y: -120px }
&.cap_YNH::before { background-position-y: -140px }
&.cap_YY::before { background-position-y: -160px }
/* Country flags */
/* Note: flag images and CSS adapted from: https://github.com/lafeber/world-flags-sprite/ */
&.country::before {
@@ -357,6 +376,7 @@
&.typ_php::before { background-position-y: -40px }
&.typ_ip::before { background-position-y: -60px }
&.typ_usr::before { background-position-y: -80px }
&.typ_rnd::before { background-position-y: -100px }
/* External link icons */
&.extlink::before { background-image: url('img/links.png') }
@@ -401,14 +421,15 @@
.page_icon::before {
content: '';
display: inline-block;
width: 20px; height: 20px;
width: 20px; min-width: 20px; max-width: 20px;
height: 20px;
background: transparent url('img/page.svg') center no-repeat;
background-position: 0 0;
background-size: 20px;
}
/* grid layout for the overview: */
.botmon_bots_grid, .botmon_webmetrics_grid, .botmon_traffic_grid {
.botmon_bots_grid, .botmon_webmetrics_grid, .botmon_traffic_grid, .botmon_captcha_grid {
& {
display: grid;
grid-gap: 0 .33em;
@@ -417,7 +438,8 @@
dd {
display: flex;
justify-content: space-between;
align-items: baseline;
align-items: center;
column-gap: .5em;
}
}
}
@@ -448,7 +470,23 @@
}
}
.botmon_traffic_grid {
grid-template-columns: 2fr 1fr;
& {
grid-template-columns: 2fr 1fr;
}
#botmon__today__wm_pages, #botmon__today__wm_referers {
& {
max-width: 100%;
overflow: hidden;
}
dd a {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
}
}
.botmon_captcha_grid {
grid-template-columns: 1fr 1fr 1fr;
}
/* The tabs bar */
@@ -527,14 +565,15 @@
color: #333;
cursor: pointer;
}
&::marker, &::before {
&::marker {
content: none;
display: none;
}
&::before {
content: '';
display: inline-block;
width: 1.25em; height: 1.25em;
width: 1.25em; min-width: 1.25em; max-width: 1.25em;
height: 1.25em;
background: transparent url('img/chevron.svg') center no-repeat;
background-size: 1.25em;
transform: rotate(-90deg);
@@ -600,8 +639,10 @@
border-radius: .5em;
}
details ul > li > details > summary {
display: flex;
justify-content: space-between;
display: grid;
grid-template-columns: 1.25em minmax(calc(100px + 4em), auto) max-content;
justify-items: stretch;
justify-content: stretch;
align-items: center;
column-gap: .5em;
font-weight: normal;
@@ -610,6 +651,7 @@
background-color: #F0F0F0;
border-bottom: #CCC solid 1px;
border-radius: .7em;
overflow: auto;
}
details ul > li > details > summary.noServer {
opacity: 67%;
@@ -620,7 +662,10 @@
column-gap: .25em;
}
details ul > li > details > summary > span:first-child {
flex-grow: 1;
justify-content: flex-start;
}
details ul > li > details > summary > span:last-child {
justify-content: flex-end;
}
details ul > li > details > summary > span > span[title] {
cursor: help;
@@ -630,8 +675,9 @@
& {
display: grid;
grid-template-columns: min-content auto;
gap: .25em .5em;
border-left: transparent none 0;
margin: 0 .5rem .25rem 0;
margin: .5rem .5rem .25rem 0;
}
dt {
grid-column: 1;
@@ -642,8 +688,6 @@
background-color: transparent;
}
dd.pages {
& {
}
ul {
li {
& {
@@ -656,17 +700,9 @@
&:nth-child(odd) {
background-color: #DFDFDF;
}
div.row {
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: baseline;
white-space: nowrap;
line-height: 1.35em;
}
span {
display: inline-block;
}
/*&.detailled {
outline: red dotted 1pt;
}*/
}
}
a[hreflang] {
@@ -685,6 +721,23 @@
padding: 0 1pt;
margin-left: .2em;
}
div.row {
& {
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: baseline;
white-space: nowrap;
line-height: 1.35em;
}
& > div {
display: inline-flex;
column-gap: .4em;
}
}
span {
display: inline-block;
}
span.first-seen {
min-width: 4.2em;
text-align: right;
@@ -693,20 +746,26 @@
font-size: smaller;
}
span.bounce {
width: 1.25em; height: 1.25em;
width: 1em; height: 1em;
overflow: hidden;
}
span.bounce::before {
display: inline-block;
content: '';
width: 1.25em; height: 1em;
width: 1em; height: 1em;
background: transparent url('img/bounce.svg') center no-repeat;
background-size: .95em;
}
span.referer {
span.referer, span.views, span.ip-address, span.user-agent {
font-size: smaller;
}
span.referer, span.ip-address, span.user-agent {
margin-left: .67rem;
}
span.user-agent {
line-break: anywhere;
text-wrap-mode: wrap;
}
}
}
@@ -730,21 +789,29 @@
background-image: url('img/info.svg')
}
/* pageviews */
span.pageviews {
/* pages seen */
span.pageseen/*, span.pageviews */{
border: #999 solid 1px;
padding: 0 2px;
font-size: smaller;
border-radius: .5em;
margin-right: .25em;
min-width: fit-content;
}
span.pageviews::before {
span.pageseen::before {
content : '';
display: inline-block;
width: 1.25em; height: 1.25em;
width: 1.25em; min-width: 1.25em; max-width: 1.25em; height: 1.25em;
background: transparent url('img/page.svg') center no-repeat;
background-size: 1.25em;
}
/*span.pageviews::before {
content : '';
display: inline-block;
width: 1.25em; height: 1.25em;
background: transparent url('img/views.svg') center no-repeat;
background-size: 1.25em;
}*/
}
/* item footer */
@@ -890,7 +957,7 @@
border-top-color: #CCC;
}
}
span.pageviews {
span.pageseen, span.pageviews {
border-color: #555;
}
@@ -929,6 +996,23 @@
}
}
/* layout overrides for narrow screens: */
@media (max-width: 1024px) {
#botmon__admin #botmon__latest {
.botmon_traffic_grid {
& {
grid-template-columns: 1fr !important;
}
dt {
margin: .5em 0;
}
dl {
border-left: transparent none 0;
padding-left: 0;
}
}
}
}
@media (max-width: 800px) {
#botmon__admin #botmon__latest #botmon__today__visitorlists {
dl.visitor_details {
@@ -952,7 +1036,7 @@
@media (max-width: 670px) {
#botmon__admin #botmon__latest {
.botmon_bots_grid, .botmon_webmetrics_grid, .botmon_traffic_grid {
.botmon_bots_grid, .botmon_webmetrics_grid, .botmon_captcha_grid {
& {
grid-template-columns: 1fr !important;
}
@@ -966,3 +1050,9 @@
}
}
}
@media (max-width: 440px) {
#botmon__admin #botmon__latest #botmon__today__content details > div { padding: .25em; }
#botmon__admin #botmon__latest #botmon__today__visitorlists details ul > li { margin-left: 0; }
#botmon__admin #botmon__latest #botmon__today__visitorlists details ul > li > details > summary { column-gap: .1em; }
}

799
admin.js

File diff suppressed because it is too large Load Diff

View File

@@ -34,6 +34,7 @@ class admin_plugin_botmon extends AdminPlugin {
// display GeoIP data?
$geoIPconf = $this->getConf('geoiplib');
$useCaptchaConf = ($this->getConf('useCaptcha') !== 'disabled');
$hasOldLogFiles = $this->hasOldLogFiles();
@@ -43,7 +44,7 @@ class admin_plugin_botmon extends AdminPlugin {
$pluginPath = $conf['basedir'] . 'lib/plugins/' . $this->getPluginName();
/* Plugin Headline */
echo '<div id="botmon__admin">
echo NL . '<div id="botmon__admin">
<h1>Bot Monitoring Plugin</h1>
<nav id="botmon__tabs">
<ul class="tabs" role="tablist">
@@ -52,11 +53,18 @@ class admin_plugin_botmon extends AdminPlugin {
</ul>
</nav>
<article role="tabpanel" id="botmon__latest">
<script>
const BMSettings = {
showday: ' . json_encode($this->getConf('showday')) . ',
combineNets: ' . json_encode($this->getConf('combineNets')) . ',
useCaptcha: ' . json_encode($this->getConf('useCaptcha') !== 'disabled') . '
};
</script>
<h2 class="a11y">Latest data</h2>
<header id="botmon__today__title">Loading&nbsp;&hellip;</header>
<div id="botmon__today__content">
<details id="botmon__today__overview" open>
<summary>Overview</summary>
<summary>Bots overview</summary>
<div class="botmon_bots_grid" data-geoip="' . $geoIPconf . '">
<dl id="botmon__today__botsvshumans"></dl>
<dl id="botmon__botslist"></dl>
@@ -79,8 +87,18 @@ class admin_plugin_botmon extends AdminPlugin {
<dl id="botmon__today__wm_pages"></dl>
<dl id="botmon__today__wm_referers"></dl>
</div>
</details>
<details id="botmon__today__visitors">
</details>' . NL;
if ($useCaptchaConf) {
echo ' <details id="botmon__today__captcha">
<summary>Captcha statistics</summary>
<div class="botmon_captcha_grid">
<dl id="botmon__today__cp_humans"></dl>
<dl id="botmon__today__cp_sus"></dl>
<dl id="botmon__today__cp_bots"></dl>
</div>
</details>' . NL;
}
echo ' <details id="botmon__today__visitors">
<summary>Visitor logs</summary>
<div id="botmon__today__visitorlists"></div>
</details>
@@ -92,7 +110,7 @@ class admin_plugin_botmon extends AdminPlugin {
</article>
<article role="tabpanel" id="botmon__log" hidden>
<h2 class="a11y">Process log</h2>
<ul id="botmon__loglist">';
<ul id="botmon__loglist">' . NL;
/* proces old logs */
if ($hasOldLogFiles) {
@@ -101,10 +119,11 @@ class admin_plugin_botmon extends AdminPlugin {
$helper->cleanup();
} else {
echo '<li>No files to process.</li>';
echo DOKU_TAB . DOKU_TAB . DOKU_TAB . '<li>No files to process.</li>' . NL;
}
echo '</article></div><!-- End of BotMon Admin Tool -->';
echo DOKU_TAB . DOKU_TAB . '</ul>' . NL . DOKU_TAB . '</article>' . NL;
echo '</div><!-- End of BotMon Admin Tool -->';
}

237
captcha.js Normal file
View File

@@ -0,0 +1,237 @@
"use strict";
/* DokuWiki BotMon Captcha JavaScript */
/* 23.10.2025 - 0.1.2 - pre-release */
/* Author: Sascha Leib <ad@hominem.info> */
const $BMCaptcha = {
init: function() {
// hide the NoJS warning:
document.getElementById('BM__NoJSWarning').close();
// install the captcha:
document.getElementsByTagName('body')[0].classList.add('botmon_captcha');
$BMCaptcha._cbDly = 1.5;
$BMCaptcha.install()
},
install: function() {
// localisation helper function:
let _loc = function(id, alt) {
if ($BMLocales && $BMLocales[id]) return $BMLocales[id];
return alt;
}
// find the parent element:
let bm_parent = document.getElementsByTagName('body')[0];
// create the dialog:
const dlg = document.createElement('dialog');
dlg.setAttribute('closedby', 'none');
dlg.setAttribute('open', 'open');
dlg.setAttribute('role', 'alertdialog');
dlg.setAttribute('aria-labelledby', 'botmon_captcha_title');
dlg.classList.add('checking');
dlg.id = 'botmon_captcha_box';
dlg.innerHTML = '<h2 id="botmon_captcha_title">' + _loc('dlgTitle', 'Title') + '</h2><p>' + _loc('dlgSubtitle', 'Subtitle') + '</p>';
// Checkbox:
const lbl = document.createElement('label');
lbl.setAttribute('aria-live', 'assertive');
lbl.innerHTML = '<span class="confirm">' + _loc('dlgConfirm', "Confirm.") + '</span>' +
'<span class="busy"></span><span class="checking">' + _loc('dlgChecking', "Checking") + '</span>' +
'<span class="loading">' + _loc('dlgLoading', "Loading") + '</span>' +
'<span class="erricon">&#65533;</span><span class="error">' + _loc('dlgError', "Error") + '</span>';
const cb = document.createElement('input');
cb.setAttribute('type', 'checkbox');
cb.setAttribute('disabled', 'disabled');
cb.addEventListener('click', $BMCaptcha._cbCallback);
lbl.prepend(cb);
dlg.appendChild(lbl);
bm_parent.appendChild(dlg);
// call the delayed callback in a couple of seconds:
$BMCaptcha._st = performance.now();
setTimeout($BMCaptcha._delayedCallback, $BMCaptcha._cbDly * 1000);
},
/* creates a digest hash */
digest: {
/* simple SHA hash function - adapted from https://geraintluff.github.io/sha256/ */
hash: function(ascii) {
// shortcut:
const sha256 = $BMCaptcha.digest.hash;
// helper function
const rightRotate = function(v, a) {
return (v>>>a) | (v<<(32 - a));
};
var mathPow = Math.pow;
var maxWord = mathPow(2, 32);
var lengthProperty = 'length'
var i, j;
var result = ''
var words = [];
var asciiBitLength = ascii[lengthProperty]*8;
//* caching results is optional - remove/add slash from front of this line to toggle
// Initial hash value: first 32 bits of the fractional parts of the square roots of the first 8 primes
// (we actually calculate the first 64, but extra values are just ignored)
var hash = sha256.h = sha256.h || [];
// Round constants: first 32 bits of the fractional parts of the cube roots of the first 64 primes
var k = sha256.k = sha256.k || [];
var primeCounter = k[lengthProperty];
/*/
var hash = [], k = [];
var primeCounter = 0;
//*/
var isComposite = {};
for (var candidate = 2; primeCounter < 64; candidate++) {
if (!isComposite[candidate]) {
for (i = 0; i < 313; i += candidate) {
isComposite[i] = candidate;
}
hash[primeCounter] = (mathPow(candidate, .5)*maxWord)|0;
k[primeCounter++] = (mathPow(candidate, 1/3)*maxWord)|0;
}
}
ascii += '\x80' // Append Ƈ' bit (plus zero padding)
while (ascii[lengthProperty]%64 - 56) ascii += '\x00' // More zero padding
for (i = 0; i < ascii[lengthProperty]; i++) {
j = ascii.charCodeAt(i);
if (j>>8) return; // ASCII check: only accept characters in range 0-255
words[i>>2] |= j << ((3 - i)%4)*8;
}
words[words[lengthProperty]] = ((asciiBitLength/maxWord)|0);
words[words[lengthProperty]] = (asciiBitLength)
// process each chunk
for (j = 0; j < words[lengthProperty];) {
var w = words.slice(j, j += 16); // The message is expanded into 64 words as part of the iteration
var oldHash = hash;
// This is now the undefinedworking hash", often labelled as variables a...g
// (we have to truncate as well, otherwise extra entries at the end accumulate
hash = hash.slice(0, 8);
for (i = 0; i < 64; i++) {
var i2 = i + j;
// Expand the message into 64 words
// Used below if
var w15 = w[i - 15], w2 = w[i - 2];
// Iterate
var a = hash[0], e = hash[4];
var temp1 = hash[7]
+ (rightRotate(e, 6) ^ rightRotate(e, 11) ^ rightRotate(e, 25)) // S1
+ ((e&hash[5])^((~e)&hash[6])) // ch
+ k[i]
// Expand the message schedule if needed
+ (w[i] = (i < 16) ? w[i] : (
w[i - 16]
+ (rightRotate(w15, 7) ^ rightRotate(w15, 18) ^ (w15>>>3)) // s0
+ w[i - 7]
+ (rightRotate(w2, 17) ^ rightRotate(w2, 19) ^ (w2>>>10)) // s1
)|0
);
// This is only used once, so *could* be moved below, but it only saves 4 bytes and makes things unreadble
var temp2 = (rightRotate(a, 2) ^ rightRotate(a, 13) ^ rightRotate(a, 22)) // S0
+ ((a&hash[1])^(a&hash[2])^(hash[1]&hash[2])); // maj
hash = [(temp1 + temp2)|0].concat(hash); // We don't bother trimming off the extra ones, they're harmless as long as we're truncating when we do the slice()
hash[4] = (hash[4] + temp1)|0;
}
for (i = 0; i < 8; i++) {
hash[i] = (hash[i] + oldHash[i])|0;
}
}
for (i = 0; i < 8; i++) {
for (j = 3; j + 1; j--) {
var b = (hash[i]>>(j*8))&255;
result += ((b < 16) ? 0 : '') + b.toString(16);
}
}
return result;
}
},
_cbCallback: function(e) {
if (e.target.checked) {
//document.getElementById('botmon_captcha_box').close();
try {
var $status = 'loading';
// generate the hash:
const dat = [ // the data to encode
document._botmon.seed || '',
location.hostname,
document._botmon.ip || '0.0.0.0',
(new Date()).toISOString().substring(0, 10)
];
if (performance.now() - $BMCaptcha._st <= 1500) dat.push(performance.now() - $BMCaptcha._st);
// set the cookie:
document.cookie = "DWConfirm=" + encodeURIComponent($BMCaptcha.digest.hash(dat.join(';'))) + '; path=/; session;';
// + (document.location.protocol === 'https:' ? ' secure;' : '');
} catch (err) {
console.error(err);
$status = 'error';
}
// change the interface:
const dlg = document.getElementById('botmon_captcha_box');
if (dlg) {
dlg.classList.add( $status );
dlg.classList.remove('ready');
}
// reload the page:
if ($status !== 'error')window.location.reload(true);
}
},
_delayedCallback: function() {
const dlg = document.getElementById('botmon_captcha_box');
if (dlg) {
dlg.classList.add('ready');
dlg.classList.remove('checking');
const input = dlg.getElementsByTagName('input')[0];
if (input) {
input.removeAttribute('disabled');
input.focus();
setTimeout($BMCaptcha._autoCheck, 200, input);
}
}
},
_cbDly: null,
_st: null,
_autoCheck: function(e) {
const bypass = ($BMConfig['captchaBypass'] || '').split(',');
var action = false;
if (bypass.indexOf('langmatch') >= 0) { // Languages matching
const cntLangs = navigator.languages.map(lang => lang.split('-')[0]);
if (cntLangs.indexOf(document.documentElement.lang || 'en') >= 0) action = true;
}
if (action) e.click(); // action!
}
}
// initialise the captcha module:
$BMCaptcha.init();

View File

@@ -5,4 +5,9 @@
* @author Sascha Leib <sascha@leib.be>
*/
$conf['showday'] = 'today';
$conf['combineNets'] = true;
$conf['geoiplib'] = 'disabled';
$conf['useCaptcha'] = 'disabled';
$conf['captchaSeed'] = 'c53bc5f94929451987efa6c768d8856b';
$conf['captchaBypass'] = '';

View File

@@ -5,5 +5,21 @@
* @author Sascha Leib <sascha@leib.be>
*/
// How to show data in the admin interface:
$meta['showday'] = array('multichoice',
'_choices' => array ('yesterday', 'today'));
$meta['combineNets'] = array('onoff');
// Geolocation settings:
$meta['geoiplib'] = array('multichoice',
'_choices' => array ('disabled', 'phpgeoip'));
// Captcha settings:
$meta['useCaptcha'] = array('multichoice',
'_choices' => array ('disabled', 'loremipsum', 'dada'));
$meta['captchaSeed'] = array('string');
$meta['captchaBypass'] = array('multicheckbox',
'_choices' => array ('langmatch'), '_other' => 'exists');

View File

@@ -2,8 +2,8 @@
"threshold": 100,
"rules": [
{"func": "fromKnownBotIP",
"id": "botIpRange", "desc": "Common Bot IP range",
"bot": 50
"id": "knownIpRange", "desc": "From known IP range",
"bot": 40
},
{"func": "matchesClient", "params": ["aol","msie","ffold","chromeold","oldedge","operaold"],
"id": "oldClient", "desc": "Obsolete browser version",
@@ -41,7 +41,7 @@
"id": "susClient", "desc": "Client identifier that is popular with bot networks",
"bot": 10
},
{"func": "matchesClient", "params": ["undici","gohttp"],
{"func": "matchesClient", "params": ["undici","gohttp","python","wget","curl"],
"id": "botClient", "desc": "Client identifier indicates web crawler",
"bot": 100
},
@@ -57,9 +57,13 @@
"id": "impPC", "desc": "Impossible combination of platform and client",
"bot": 70
},
{"func": "loadSpeed", "params": [3, 10],
"id": "speedRun", "desc": "Average time between page loads is less than 10 seconds",
"bot": 30
},
{"func": "loadSpeed", "params": [3, 20],
"id": "speedRun", "desc": "Average time between page loads is less than 20 seconds",
"bot": 60
"bot": 30
},
{"func": "noAcceptLang",
"id": "noAcc", "desc": "No “Accept-Language” header",
@@ -69,9 +73,13 @@
"id": "langMatch", "desc": "Clients Accept-Language header does not match the page language",
"bot": 30
},
{"func": "matchesClient", "params": ["whatsapp","applemsgs","goognblm","tiktok","meta","chatgpt","claude","perplexity"],
"id": "previewClient", "desc": "User-triggered bot load (e.g. preview)",
"bot": -120
{"func": "blockedByCaptcha", "params": [],
"id": "blockedByCaptcha", "desc": "Visitor did not solve the captcha",
"bot": 50
},
{"func": "whitelistedByCaptcha", "params": [],
"id": "whitelistedByCaptcha", "desc": "Visitor uses a whitelisted IP address",
"bot": -30
}
]
}

View File

@@ -0,0 +1,354 @@
# Internet Archive Bot Ranges
207.241.224.0 207.241.239.255 20
207.241.224.0 207.241.224.255 24
207.241.231.0 207.241.231.255 24
207.241.234.0 207.241.234.255 24
207.241.237.0 207.241.237.255 24
208.70.24.0 208.70.31.255 21
# Bing Bot IP ranges - taken from https://www.bing.com/toolbox/bingbot.json
157.55.39.0 157.55.39.255 24
207.46.13.0 207.46.13.255 24
40.77.167.0 40.77.167.255 24
13.66.139.0 13.66.139.255 24
13.66.144.0 13.66.144.255 24
52.167.144.0 52.167.144.255 24
13.67.10.16 13.67.10.31 28
13.69.66.240 13.69.66.255 28
13.71.172.224 13.71.172.239 28
139.217.52.0 139.217.52.15 28
191.233.204.224 191.233.204.239 28
20.36.108.32 20.36.108.47 28
20.43.120.16 20.43.120.31 28
40.79.131.208 40.79.131.223 28
40.79.186.176 40.79.186.191 28
52.231.148.0 52.231.148.15 28
20.79.107.240 20.79.107.255 28
51.105.67.0 51.105.67.15 28
20.125.163.80 20.125.163.95 28
40.77.188.0 40.77.188.255 22
65.55.210.0 65.55.210.255 24
199.30.24.0 199.30.24.255 23
40.77.202.0 40.77.202.255 24
40.77.139.0 40.77.139.127 25
20.74.197.0 20.74.197.15 28
20.15.133.160 20.15.133.191 27
40.77.177.0 40.77.177.255 24
40.77.178.0 40.77.178.255 23
# Google Bot IP ranges - taken from: https://developers.google.com/static/search/apis/ipranges/googlebot.json
192.178.4.0 192.178.4.32 27
192.178.4.128 192.178.4.159 27
192.178.4.160 192.178.4.191 27
192.178.4.192 192.178.4.223 27
192.178.4.32 192.178.4.63 27
192.178.4.64 192.178.4.95 27
192.178.4.96 192.178.4.127 27
192.178.5.0 192.178.5.32 27
192.178.6.0 192.178.6.32 27
192.178.6.128 192.178.6.159 27
192.178.6.160 192.178.6.191 27
192.178.6.192 192.178.6.223 27
192.178.6.224 192.178.6.255 27
192.178.6.32 192.178.6.63 27
192.178.6.64 192.178.6.95 27
192.178.6.96 192.178.6.127 27
192.178.7.0 192.178.7.32 27
192.178.7.128 192.178.7.159 27
192.178.7.160 192.178.7.191 27
192.178.7.192 192.178.7.223 27
192.178.7.224 192.178.7.255 27
192.178.7.32 192.178.7.63 27
192.178.7.64 192.178.7.95 27
192.178.7.96 192.178.7.127 27
34.100.182.96 34.100.182.111 28
34.101.50.144 34.101.50.159 28
34.118.254.0 34.118.254.15 28
34.118.66.0 34.118.66.15 28
34.126.178.96 34.126.178.111 28
34.146.150.144 34.146.150.159 28
34.147.110.144 34.147.110.159 28
34.151.74.144 34.151.74.159 28
34.152.50.64 34.152.50.79 28
34.154.114.144 34.154.114.159 28
34.155.98.32 34.155.98.47 28
34.165.18.176 34.165.18.191 28
34.175.160.64 34.175.160.79 28
34.176.130.16 34.176.130.31 28
34.22.85.0 34.22.85.32 27
34.64.82.64 34.64.82.79 28
34.65.242.112 34.65.242.127 28
34.80.50.80 34.80.50.95 28
34.88.194.0 34.88.194.15 28
34.89.10.80 34.89.10.95 28
34.89.198.80 34.89.198.95 28
34.96.162.48 34.96.162.63 28
35.247.243.240 35.247.243.255 28
66.249.64.0 66.249.64.32 27
66.249.64.128 66.249.64.159 27
66.249.64.160 66.249.64.191 27
66.249.64.192 66.249.64.223 27
66.249.64.224 66.249.64.255 27
66.249.64.32 66.249.64.63 27
66.249.64.64 66.249.64.95 27
66.249.64.96 66.249.64.127 27
66.249.65.0 66.249.65.32 27
66.249.65.128 66.249.65.159 27
66.249.65.160 66.249.65.191 27
66.249.65.192 66.249.65.223 27
66.249.65.224 66.249.65.255 27
66.249.65.32 66.249.65.63 27
66.249.65.64 66.249.65.95 27
66.249.65.96 66.249.65.127 27
66.249.66.0 66.249.66.32 27
66.249.66.128 66.249.66.159 27
66.249.66.160 66.249.66.191 27
66.249.66.192 66.249.66.223 27
66.249.66.224 66.249.66.255 27
66.249.66.32 66.249.66.63 27
66.249.66.64 66.249.66.95 27
66.249.66.96 66.249.66.127 27
66.249.67.0 66.249.67.32 27
66.249.67.32 66.249.67.63 27
66.249.68.0 66.249.68.32 27
66.249.68.128 66.249.68.159 27
66.249.68.160 66.249.68.191 27
66.249.68.192 66.249.68.223 27
66.249.68.32 66.249.68.63 27
66.249.68.64 66.249.68.95 27
66.249.68.96 66.249.68.127 27
66.249.69.0 66.249.69.32 27
66.249.69.128 66.249.69.159 27
66.249.69.160 66.249.69.191 27
66.249.69.192 66.249.69.223 27
66.249.69.224 66.249.69.255 27
66.249.69.32 66.249.69.63 27
66.249.69.64 66.249.69.95 27
66.249.69.96 66.249.69.127 27
66.249.70.0 66.249.70.32 27
66.249.70.128 66.249.70.159 27
66.249.70.160 66.249.70.191 27
66.249.70.192 66.249.70.223 27
66.249.70.224 66.249.70.255 27
66.249.70.32 66.249.70.63 27
66.249.70.64 66.249.70.95 27
66.249.70.96 66.249.70.127 27
66.249.71.0 66.249.71.32 27
66.249.71.128 66.249.71.159 27
66.249.71.160 66.249.71.191 27
66.249.71.192 66.249.71.223 27
66.249.71.224 66.249.71.255 27
66.249.71.32 66.249.71.63 27
66.249.71.64 66.249.71.95 27
66.249.71.96 66.249.71.127 27
66.249.72.0 66.249.72.32 27
66.249.72.128 66.249.72.159 27
66.249.72.160 66.249.72.191 27
66.249.72.192 66.249.72.223 27
66.249.72.224 66.249.72.255 27
66.249.72.32 66.249.72.63 27
66.249.72.64 66.249.72.95 27
66.249.73.0 66.249.73.32 27
66.249.73.128 66.249.73.159 27
66.249.73.160 66.249.73.191 27
66.249.73.192 66.249.73.223 27
66.249.73.224 66.249.73.255 27
66.249.73.32 66.249.73.63 27
66.249.73.64 66.249.73.95 27
66.249.73.96 66.249.73.127 27
66.249.74.0 66.249.74.32 27
66.249.74.128 66.249.74.159 27
66.249.74.160 66.249.74.191 27
66.249.74.192 66.249.74.223 27
66.249.74.224 66.249.74.255 27
66.249.74.32 66.249.74.63 27
66.249.74.64 66.249.74.95 27
66.249.74.96 66.249.74.127 27
66.249.75.0 66.249.75.32 27
66.249.75.128 66.249.75.159 27
66.249.75.160 66.249.75.191 27
66.249.75.192 66.249.75.223 27
66.249.75.224 66.249.75.255 27
66.249.75.32 66.249.75.63 27
66.249.75.64 66.249.75.95 27
66.249.75.96 66.249.75.127 27
66.249.76.0 66.249.76.32 27
66.249.76.128 66.249.76.159 27
66.249.76.160 66.249.76.191 27
66.249.76.192 66.249.76.223 27
66.249.76.224 66.249.76.255 27
66.249.76.32 66.249.76.63 27
66.249.76.64 66.249.76.95 27
66.249.76.96 66.249.76.127 27
66.249.77.0 66.249.77.32 27
66.249.77.128 66.249.77.159 27
66.249.77.160 66.249.77.191 27
66.249.77.192 66.249.77.223 27
66.249.77.224 66.249.77.255 27
66.249.77.32 66.249.77.63 27
66.249.77.64 66.249.77.95 27
66.249.77.96 66.249.77.127 27
66.249.78.0 66.249.78.32 27
66.249.78.128 66.249.78.159 27
66.249.78.160 66.249.78.191 27
66.249.78.32 66.249.78.63 27
66.249.78.64 66.249.78.95 27
66.249.78.96 66.249.78.127 27
66.249.79.0 66.249.79.32 27
66.249.79.128 66.249.79.159 27
66.249.79.160 66.249.79.191 27
66.249.79.192 66.249.79.223 27
66.249.79.224 66.249.79.255 27
66.249.79.32 66.249.79.63 27
66.249.79.64 66.249.79.95 27
2001:4860:4801:0010:: 2001:4860:4801:0010:FFFF:FFFF:FFFF:FFFF 64
2001:4860:4801:0012:: 2001:4860:4801:0012:FFFF:FFFF:FFFF:FFFF 64
2001:4860:4801:0013:: 2001:4860:4801:0013:FFFF:FFFF:FFFF:FFFF 64
2001:4860:4801:0014:: 2001:4860:4801:0014:FFFF:FFFF:FFFF:FFFF 64
2001:4860:4801:0015:: 2001:4860:4801:0015:FFFF:FFFF:FFFF:FFFF 64
2001:4860:4801:0016:: 2001:4860:4801:0016:FFFF:FFFF:FFFF:FFFF 64
2001:4860:4801:0017:: 2001:4860:4801:0017:FFFF:FFFF:FFFF:FFFF 64
2001:4860:4801:0018:: 2001:4860:4801:0018:FFFF:FFFF:FFFF:FFFF 64
2001:4860:4801:0019:: 2001:4860:4801:0019:FFFF:FFFF:FFFF:FFFF 64
2001:4860:4801:001a:: 2001:4860:4801:001a:FFFF:FFFF:FFFF:FFFF 64
2001:4860:4801:001b:: 2001:4860:4801:001b:FFFF:FFFF:FFFF:FFFF 64
2001:4860:4801:001c:: 2001:4860:4801:001c:FFFF:FFFF:FFFF:FFFF 64
2001:4860:4801:001d:: 2001:4860:4801:001d:FFFF:FFFF:FFFF:FFFF 64
2001:4860:4801:001e:: 2001:4860:4801:001e:FFFF:FFFF:FFFF:FFFF 64
2001:4860:4801:001f:: 2001:4860:4801:001f:FFFF:FFFF:FFFF:FFFF 64
2001:4860:4801:0020:: 2001:4860:4801:0020:FFFF:FFFF:FFFF:FFFF 64
2001:4860:4801:0021:: 2001:4860:4801:0021:FFFF:FFFF:FFFF:FFFF 64
2001:4860:4801:0022:: 2001:4860:4801:0022:FFFF:FFFF:FFFF:FFFF 64
2001:4860:4801:0023:: 2001:4860:4801:0023:FFFF:FFFF:FFFF:FFFF 64
2001:4860:4801:0024:: 2001:4860:4801:0024:FFFF:FFFF:FFFF:FFFF 64
2001:4860:4801:0025:: 2001:4860:4801:0025:FFFF:FFFF:FFFF:FFFF 64
2001:4860:4801:0026:: 2001:4860:4801:0026:FFFF:FFFF:FFFF:FFFF 64
2001:4860:4801:0027:: 2001:4860:4801:0027:FFFF:FFFF:FFFF:FFFF 64
2001:4860:4801:0028:: 2001:4860:4801:0028:FFFF:FFFF:FFFF:FFFF 64
2001:4860:4801:0029:: 2001:4860:4801:0029:FFFF:FFFF:FFFF:FFFF 64
2001:4860:4801:0002:: 2001:4860:4801:0002:FFFF:FFFF:FFFF:FFFF 64
2001:4860:4801:002a:: 2001:4860:4801:002a:FFFF:FFFF:FFFF:FFFF 64
2001:4860:4801:002b:: 2001:4860:4801:002b:FFFF:FFFF:FFFF:FFFF 64
2001:4860:4801:002c:: 2001:4860:4801:002c:FFFF:FFFF:FFFF:FFFF 64
2001:4860:4801:002d:: 2001:4860:4801:002d:FFFF:FFFF:FFFF:FFFF 64
2001:4860:4801:002e:: 2001:4860:4801:002e:FFFF:FFFF:FFFF:FFFF 64
2001:4860:4801:002f:: 2001:4860:4801:002f:FFFF:FFFF:FFFF:FFFF 64
2001:4860:4801:0030:: 2001:4860:4801:0030:FFFF:FFFF:FFFF:FFFF 64
2001:4860:4801:0031:: 2001:4860:4801:0031:FFFF:FFFF:FFFF:FFFF 64
2001:4860:4801:0032:: 2001:4860:4801:0032:FFFF:FFFF:FFFF:FFFF 64
2001:4860:4801:0033:: 2001:4860:4801:0033:FFFF:FFFF:FFFF:FFFF 64
2001:4860:4801:0034:: 2001:4860:4801:0034:FFFF:FFFF:FFFF:FFFF 64
2001:4860:4801:0035:: 2001:4860:4801:0035:FFFF:FFFF:FFFF:FFFF 64
2001:4860:4801:0036:: 2001:4860:4801:0036:FFFF:FFFF:FFFF:FFFF 64
2001:4860:4801:0037:: 2001:4860:4801:0037:FFFF:FFFF:FFFF:FFFF 64
2001:4860:4801:0038:: 2001:4860:4801:0038:FFFF:FFFF:FFFF:FFFF 64
2001:4860:4801:0039:: 2001:4860:4801:0039:FFFF:FFFF:FFFF:FFFF 64
2001:4860:4801:003a:: 2001:4860:4801:003a:FFFF:FFFF:FFFF:FFFF 64
2001:4860:4801:003b:: 2001:4860:4801:003b:FFFF:FFFF:FFFF:FFFF 64
2001:4860:4801:003c:: 2001:4860:4801:003c:FFFF:FFFF:FFFF:FFFF 64
2001:4860:4801:003d:: 2001:4860:4801:003d:FFFF:FFFF:FFFF:FFFF 64
2001:4860:4801:003e:: 2001:4860:4801:003e:FFFF:FFFF:FFFF:FFFF 64
2001:4860:4801:003f:: 2001:4860:4801:003f:FFFF:FFFF:FFFF:FFFF 64
2001:4860:4801:0040:: 2001:4860:4801:0040:FFFF:FFFF:FFFF:FFFF 64
2001:4860:4801:0041:: 2001:4860:4801:0041:FFFF:FFFF:FFFF:FFFF 64
2001:4860:4801:0042:: 2001:4860:4801:0042:FFFF:FFFF:FFFF:FFFF 64
2001:4860:4801:0044:: 2001:4860:4801:0044:FFFF:FFFF:FFFF:FFFF 64
2001:4860:4801:0045:: 2001:4860:4801:0045:FFFF:FFFF:FFFF:FFFF 64
2001:4860:4801:0046:: 2001:4860:4801:0046:FFFF:FFFF:FFFF:FFFF 64
2001:4860:4801:0047:: 2001:4860:4801:0047:FFFF:FFFF:FFFF:FFFF 64
2001:4860:4801:0048:: 2001:4860:4801:0048:FFFF:FFFF:FFFF:FFFF 64
2001:4860:4801:0049:: 2001:4860:4801:0049:FFFF:FFFF:FFFF:FFFF 64
2001:4860:4801:004a:: 2001:4860:4801:004a:FFFF:FFFF:FFFF:FFFF 64
2001:4860:4801:004b:: 2001:4860:4801:004b:FFFF:FFFF:FFFF:FFFF 64
2001:4860:4801:004c:: 2001:4860:4801:004c:FFFF:FFFF:FFFF:FFFF 64
2001:4860:4801:004d:: 2001:4860:4801:004d:FFFF:FFFF:FFFF:FFFF 64
2001:4860:4801:004e:: 2001:4860:4801:004e:FFFF:FFFF:FFFF:FFFF 64
2001:4860:4801:0050:: 2001:4860:4801:0050:FFFF:FFFF:FFFF:FFFF 64
2001:4860:4801:0051:: 2001:4860:4801:0051:FFFF:FFFF:FFFF:FFFF 64
2001:4860:4801:0052:: 2001:4860:4801:0052:FFFF:FFFF:FFFF:FFFF 64
2001:4860:4801:0053:: 2001:4860:4801:0053:FFFF:FFFF:FFFF:FFFF 64
2001:4860:4801:0054:: 2001:4860:4801:0054:FFFF:FFFF:FFFF:FFFF 64
2001:4860:4801:0055:: 2001:4860:4801:0055:FFFF:FFFF:FFFF:FFFF 64
2001:4860:4801:0056:: 2001:4860:4801:0056:FFFF:FFFF:FFFF:FFFF 64
2001:4860:4801:0057:: 2001:4860:4801:0057:FFFF:FFFF:FFFF:FFFF 64
2001:4860:4801:0058:: 2001:4860:4801:0058:FFFF:FFFF:FFFF:FFFF 64
2001:4860:4801:0060:: 2001:4860:4801:0060:FFFF:FFFF:FFFF:FFFF 64
2001:4860:4801:0061:: 2001:4860:4801:0061:FFFF:FFFF:FFFF:FFFF 64
2001:4860:4801:0062:: 2001:4860:4801:0062:FFFF:FFFF:FFFF:FFFF 64
2001:4860:4801:0063:: 2001:4860:4801:0063:FFFF:FFFF:FFFF:FFFF 64
2001:4860:4801:0064:: 2001:4860:4801:0064:FFFF:FFFF:FFFF:FFFF 64
2001:4860:4801:0065:: 2001:4860:4801:0065:FFFF:FFFF:FFFF:FFFF 64
2001:4860:4801:0066:: 2001:4860:4801:0066:FFFF:FFFF:FFFF:FFFF 64
2001:4860:4801:0067:: 2001:4860:4801:0067:FFFF:FFFF:FFFF:FFFF 64
2001:4860:4801:0068:: 2001:4860:4801:0068:FFFF:FFFF:FFFF:FFFF 64
2001:4860:4801:0069:: 2001:4860:4801:0069:FFFF:FFFF:FFFF:FFFF 64
2001:4860:4801:006a:: 2001:4860:4801:006a:FFFF:FFFF:FFFF:FFFF 64
2001:4860:4801:006b:: 2001:4860:4801:006b:FFFF:FFFF:FFFF:FFFF 64
2001:4860:4801:006c:: 2001:4860:4801:006c:FFFF:FFFF:FFFF:FFFF 64
2001:4860:4801:006d:: 2001:4860:4801:006d:FFFF:FFFF:FFFF:FFFF 64
2001:4860:4801:006e:: 2001:4860:4801:006e:FFFF:FFFF:FFFF:FFFF 64
2001:4860:4801:006f:: 2001:4860:4801:006f:FFFF:FFFF:FFFF:FFFF 64
2001:4860:4801:0070:: 2001:4860:4801:0070:FFFF:FFFF:FFFF:FFFF 64
2001:4860:4801:0071:: 2001:4860:4801:0071:FFFF:FFFF:FFFF:FFFF 64
2001:4860:4801:0072:: 2001:4860:4801:0072:FFFF:FFFF:FFFF:FFFF 64
2001:4860:4801:0073:: 2001:4860:4801:0073:FFFF:FFFF:FFFF:FFFF 64
2001:4860:4801:0074:: 2001:4860:4801:0074:FFFF:FFFF:FFFF:FFFF 64
2001:4860:4801:0075:: 2001:4860:4801:0075:FFFF:FFFF:FFFF:FFFF 64
2001:4860:4801:0076:: 2001:4860:4801:0076:FFFF:FFFF:FFFF:FFFF 64
2001:4860:4801:0077:: 2001:4860:4801:0077:FFFF:FFFF:FFFF:FFFF 64
2001:4860:4801:0078:: 2001:4860:4801:0078:FFFF:FFFF:FFFF:FFFF 64
2001:4860:4801:0079:: 2001:4860:4801:0079:FFFF:FFFF:FFFF:FFFF 64
2001:4860:4801:007a:: 2001:4860:4801:007a:FFFF:FFFF:FFFF:FFFF 64
2001:4860:4801:007b:: 2001:4860:4801:007b:FFFF:FFFF:FFFF:FFFF 64
2001:4860:4801:007c:: 2001:4860:4801:007c:FFFF:FFFF:FFFF:FFFF 64
2001:4860:4801:007d:: 2001:4860:4801:007d:FFFF:FFFF:FFFF:FFFF 64
2001:4860:4801:0080:: 2001:4860:4801:0080:FFFF:FFFF:FFFF:FFFF 64
2001:4860:4801:0081:: 2001:4860:4801:0081:FFFF:FFFF:FFFF:FFFF 64
2001:4860:4801:0082:: 2001:4860:4801:0082:FFFF:FFFF:FFFF:FFFF 64
2001:4860:4801:0083:: 2001:4860:4801:0083:FFFF:FFFF:FFFF:FFFF 64
2001:4860:4801:0084:: 2001:4860:4801:0084:FFFF:FFFF:FFFF:FFFF 64
2001:4860:4801:0085:: 2001:4860:4801:0085:FFFF:FFFF:FFFF:FFFF 64
2001:4860:4801:0086:: 2001:4860:4801:0086:FFFF:FFFF:FFFF:FFFF 64
2001:4860:4801:0087:: 2001:4860:4801:0087:FFFF:FFFF:FFFF:FFFF 64
2001:4860:4801:0088:: 2001:4860:4801:0088:FFFF:FFFF:FFFF:FFFF 64
2001:4860:4801:0090:: 2001:4860:4801:0090:FFFF:FFFF:FFFF:FFFF 64
2001:4860:4801:0091:: 2001:4860:4801:0091:FFFF:FFFF:FFFF:FFFF 64
2001:4860:4801:0092:: 2001:4860:4801:0092:FFFF:FFFF:FFFF:FFFF 64
2001:4860:4801:0093:: 2001:4860:4801:0093:FFFF:FFFF:FFFF:FFFF 64
2001:4860:4801:0094:: 2001:4860:4801:0094:FFFF:FFFF:FFFF:FFFF 64
2001:4860:4801:0095:: 2001:4860:4801:0095:FFFF:FFFF:FFFF:FFFF 64
2001:4860:4801:0096:: 2001:4860:4801:0096:FFFF:FFFF:FFFF:FFFF 64
2001:4860:4801:0097:: 2001:4860:4801:0097:FFFF:FFFF:FFFF:FFFF 64
2001:4860:4801:00a0:: 2001:4860:4801:00a0:FFFF:FFFF:FFFF:FFFF 64
2001:4860:4801:00a1:: 2001:4860:4801:00a1:FFFF:FFFF:FFFF:FFFF 64
2001:4860:4801:00a2:: 2001:4860:4801:00a2:FFFF:FFFF:FFFF:FFFF 64
2001:4860:4801:00a3:: 2001:4860:4801:00a3:FFFF:FFFF:FFFF:FFFF 64
2001:4860:4801:00a4:: 2001:4860:4801:00a4:FFFF:FFFF:FFFF:FFFF 64
2001:4860:4801:00a5:: 2001:4860:4801:00a5:FFFF:FFFF:FFFF:FFFF 64
2001:4860:4801:00a6:: 2001:4860:4801:00a6:FFFF:FFFF:FFFF:FFFF 64
2001:4860:4801:00a7:: 2001:4860:4801:00a7:FFFF:FFFF:FFFF:FFFF 64
2001:4860:4801:00a8:: 2001:4860:4801:00a8:FFFF:FFFF:FFFF:FFFF 64
2001:4860:4801:00a9:: 2001:4860:4801:00a9:FFFF:FFFF:FFFF:FFFF 64
2001:4860:4801:00aa:: 2001:4860:4801:00aa:FFFF:FFFF:FFFF:FFFF 64
2001:4860:4801:00ab:: 2001:4860:4801:00ab:FFFF:FFFF:FFFF:FFFF 64
2001:4860:4801:00ac:: 2001:4860:4801:00ac:FFFF:FFFF:FFFF:FFFF 64
2001:4860:4801:00ad:: 2001:4860:4801:00ad:FFFF:FFFF:FFFF:FFFF 64
2001:4860:4801:00ae:: 2001:4860:4801:00ae:FFFF:FFFF:FFFF:FFFF 64
2001:4860:4801:00b0:: 2001:4860:4801:00b0:FFFF:FFFF:FFFF:FFFF 64
2001:4860:4801:00b1:: 2001:4860:4801:00b1:FFFF:FFFF:FFFF:FFFF 64
2001:4860:4801:00b2:: 2001:4860:4801:00b2:FFFF:FFFF:FFFF:FFFF 64
2001:4860:4801:00b3:: 2001:4860:4801:00b3:FFFF:FFFF:FFFF:FFFF 64
2001:4860:4801:00b4:: 2001:4860:4801:00b4:FFFF:FFFF:FFFF:FFFF 64
2001:4860:4801:00b5:: 2001:4860:4801:00b5:FFFF:FFFF:FFFF:FFFF 64
2001:4860:4801:000c:: 2001:4860:4801:000c:FFFF:FFFF:FFFF:FFFF 64
2001:4860:4801:000f:: 2001:4860:4801:000f:FFFF:FFFF:FFFF:FFFF 64
# SeznamBot - IPs from: https://o-seznam.cz/napoveda/vyhledavani/en/seznambot-crawler/
77.75.76.0 77.75.79.255 22
2a02:0598:0064:8a00:0000:0000:3100:0000 2a02:0598:0064:8a00:0000:0000:3100:001f 123
2a02:0598:0128:8a00:0000:0000:0b00:0000 2a02:0598:0128:8a00:0000:0000:0b00:001f 123
2a02:0598:0096:8a00:0000:0000:1200:0120 2a02:0598:0096:8a00:0000:0000:1200:013f 123
# localhosts
127.0.0.1 127.255.255.255 8
::1 ::1 128

View File

@@ -7,7 +7,7 @@
{"id": "googlebot",
"n": "GoogleBot",
"r": ["Googlebot"],
"rx": ["Googlebot\\/(\\d+\\.\\d+)", "Googlebot-Image\\/(\\d+\\.\\d+)"],
"rx": ["Googlebot\\/(\\d+\\.\\d+)", "Googlebot-Image\\/(\\d+\\.\\d+)","\\sGoogleOther(\\-\\w+)?[\\)\\/]"],
"url": "http://www.google.com/bot.html"
},
{"id": "googleads",
@@ -22,12 +22,6 @@
"rx": ["APIs-Google"],
"url": "https://developers.google.com/search/docs/crawling-indexing/google-special-case-crawlers"
},
{"id": "googleother",
"n": "GoogleOther",
"r": ["GoogleOther"],
"rx": ["\\sGoogleOther(\\-\\w+)?[\\)\\/]"],
"url": "https://developers.google.com/search/docs/crawling-indexing/google-common-crawlers#googleother"
},
{"id": "googinspct",
"n": "Google-InspectionTool",
"r": ["Google-InspectionTool"],
@@ -118,16 +112,16 @@
"rx": ["Perplexity\\-User\\/(\\d+\\.\\d+);"],
"url": "https://perplexity.ai/perplexitybot"
},
{"id": "metabots",
"n": "Meta/Facebook",
"r": ["meta-webindexer","meta-externalads","meta-externalagent"],
"rx": ["facebook[cw]\\w+\\/(\\d+\\.\\d+)", "meta-externala\\w+\\/(\\d+\\.\\d+)"],
"url": "https://developers.facebook.com/docs/sharing/webmasters/crawler"
},
{"id": "metauser",
"n": "Meta/Facebook User",
"r": ["facebookexternalhit","facebookcatalog"],
"rx": ["facebookexternalhit\\/(\\d+\\.?\\d*)", "meta-externalfetcher\\/(\\d\\.\\d)", "facebookcatalog\\/(\\d+\\.?\\d*)"],
"rx": ["facebookexternalhit\\/(\\d+\\.?\\d*)", "facebookcatalog\\/(\\d\\.?\\d*)"],
"url": "https://developers.facebook.com/docs/sharing/webmasters/crawler"
},
{"id": "metabots",
"n": "Meta/Facebook",
"r": ["meta-webindexer","meta-externalads","meta-externalagent", "meta-webindexer"],
"rx": ["facebook[cw]\\w+\\/(\\d+\\.?\\d*)", "meta\\-[cw]\\w+\\/(\\d+\\.?\\d*)", "meta-externalads\\/(\\d+\\.?\\d*)", "meta-externalagent\\/(\\d+\\.?\\d*)"],
"url": "https://developers.facebook.com/docs/sharing/webmasters/crawler"
},
{"id": "qwant",

View File

@@ -87,12 +87,24 @@
"id": "undici",
"rx": ["undici"]
},
{"n": "GoHTTP-based crawler",
{"n": "GoHTTP-based bot",
"id": "gohttp",
"rx": ["Go\\-http\\-client\\/(\\d+)", "quic\\-go\\-HTTP\\/(\\d+)"]
},
{"n": "Python URLlib-based crawler",
"id": "python",
"rx": ["Python\\-?\\w*\\/(\\d+\\.?\\d*)"]
},
{"n": "AppleWebKit",
"id": "webkit",
"rx": [ "AppleWebKit\\/(\\d*)\\." ]
},
{"n": "wget",
"id": "wget",
"rx": [ "Wget\\/(\\d+\\.?\\d*\\.?\\d*)" ]
},
{"n": "PrivacyBrowser",
"id": "privacybrowser",
"rx": [ "PrivacyBrowser\\/(\\d+\\.?\\d*)" ]
}
]

View File

@@ -1,23 +1,25 @@
{
"groups": [
{"id": "alibaba", "name": "Alibaba"},
{"id": "amazon", "name": "Amazon DS"},
{"id": "brasilnet", "name": "BrasilNet"},
{"id": "charter", "name": "Charter Inc."},
{"id": "chinanet", "name": "Chinanet"},
{"id": "cnisp", "name": "China ISP"},
{"id": "alibaba", "name": "Alibaba Network"},
{"id": "amazon", "name": "Amazon Data Centres"},
{"id": "bezeq", "name": "Bezeq Int."},
{"id": "charter", "name": "Charter Inc. Range"},
{"id": "chinanet", "name": "ChinaNet"},
{"id": "cnisp", "name": "China ISP Range"},
{"id": "cnmob", "name": "China Mobile"},
{"id": "google", "name": "Google LLC"},
{"id": "domtehniki", "name": "Dom Tehniki / WS Telecom"},
{"id": "google", "name": "Google LLC Network"},
{"id": "hetzner", "name": "Hetzner US"},
{"id": "huawei", "name": "Huawei"},
{"id": "misc_sa", "name": "Misc. SA ISPs"},
{"id": "tencent", "name": "Tencent"},
{"id": "huawei", "name": "Huawei Network"},
{"id": "netcup", "name": "netcup GmbH"},
{"id": "tencent", "name": "Tencent Network"},
{"id": "unicom", "name": "China Unicom"},
{"id": "vnpt", "name": "Vietnam Telecom"},
{"id": "vdsina", "name": "VDSina NL"},
{"id": "zenlayer", "name": "Zenlayer"}
{"id": "vdsina", "name": "VDSina Network"},
{"id": "zenlayer", "name": "Zenlayer Network"}
],
"ranges": [
{"from": "1.92.0.0", "to": "1.95.255.254", "m": 14, "g": "huawei"},
{"from": "3.0.0.0", "to": "3.255.255.254", "m": 8, "g": "amazon"},
{"from": "5.161.0.0", "to": "5.161.255.255", "m": 16, "g": "hetzner"},
{"from": "8.128.0.0", "to": "8.191.255.254", "m": 10, "g": "alibaba"},
@@ -32,6 +34,7 @@
{"from": "39.64.0.0", "to": "39.95.255.254", "g": "cnmob"},
{"from": "43.132.0.0", "to": "43.132.255.254", "m": 16, "g": "tencent"},
{"from": "43.133.0.0", "to": "43.133.255.254", "m": 16, "g": "tencent"},
{"from": "43.173.128.0", "to": "43.191.255.255", "m": 10, "g": "tencent"},
{"from": "44.192.0.0", "to": "44.255.255.254", "m": 16, "g": "tencent"},
{"from": "45.0.0.0", "to": "45.255.255.254", "m": 10, "g": "amazon"},
{"from": "46.250.160.0", "to": "46.250.191.254", "m": 19, "g": "huawei"},
@@ -44,7 +47,7 @@
{"from": "52.96.0.0", "to": "52.127.255.254", "m": 11, "g": "microsoft"},
{"from": "54.0.0.0", "to": "54.255.255.254", "m": 8, "g": "amazon"},
{"from": "66.249.64.0", "to": "66.249.95.254", "m": 19, "g": "google"},
{"from": "84.37.35.0", "to": "84.37.255.254", "g": "gtt"},
{"from": "82.80.0.0", "to": "82.81.255.255", "m": 15, "g": "bezeq"},
{"from": "94.74.64.0", "to": "94.74.127.254", "m": 18, "g": "huawei"},
{"from": "91.84.96.0", "to": "91.84.127.254", "m": 19, "g": "vdsina"},
{"from": "101.0.0.0", "to": "101.255.255.254", "m": 8,"g": "chinanet"},
@@ -53,41 +56,29 @@
{"from": "111.119.192.0", "to": "111.119.255.254", "m": 18, "g": "huawei"},
{"from": "113.160.0.0", "to": "113.191.255.254", "m": 11, "g": "vnpt"},
{"from": "114.208.0.0", "to": "114.223.255.254", "m": 12, "g": "unicom"},
{"from": "114.119.0.0", "to": "114.119.255.254", "m": 11, "g": "huawei"},
{"from": "114.224.0.0", "to": "114.255.255.254", "m": 11, "g": "unicom"},
{"from": "119.8.0.0", "to": "119.8.255.254", "m": 16, "g": "huawei"},
{"from": "119.12.160.0", "to": "119.12.175.255", "m": 20, "g": "huawei"},
{"from": "119.13.0.0", "to": "119.13.255.254", "m": 16, "g": "huawei"},
{"from": "121.91.168.0", "to": "121.91.175.254", "m": 21, "g": "huawei"},
{"from": "122.8.0.0", "to": "122.8.255.254", "g": "cnisp"},
{"from": "122.9.0.0", "to": "122.9.255.254", "m": 16, "g": "huawei"},
{"from": "123.16.0.0", "to": "123.31.255.254", "m": 12, "g": "vnpt"},
{"from": "124.243.128.0", "to": "124.243.191.254", "m": 18, "g": "huawei"},
{"from": "138.59.0.0", "to": "138.59.225.254", "m": 16, "g": "misc_sa"},
{"from": "138.121.0.0", "to": "138.121.225.254", "m": 16, "g": "misc_sa"},
{"from": "136.107.0.0", "to": "136.125.255.254", "m": "+", "g": "google"},
{"from": "142.147.128.0", "to": "1142.147.255.254", "m": 17, "g": "w2obj"},
{"from": "146.174.128.0", "to": "146.174.191.254", "m": 18, "g": "huawei"},
{"from": "149.126.192.0", "to": "149.126.223.255", "m": 19, "g": "domtehniki"},
{"from": "149.232.128.0", "to": "149.232.159.254", "m": 19, "g": "huawei"},
{"from": "150.40.128.0", "to": "150.40.255.254", "m": 17, "g": "huawei"},
{"from": "159.138.0.0", "to": "159.138.225.254", "m": 16, "g": "huawei"},
{"from": "162.128.0.0", "to": "162.128.255.254", "m": 16, "g": "zenlayer"},
{"from": "166.108.192.0", "to": "166.108.255.254", "m": 18, "g": "huawei"},
{"from": "168.232.192.0", "to": "168.232.255.254", "m": 16, "g": "misc_sa"},
{"from": "170.0.0.0", "to": "170.3.255.254", "m": 14, "g": "misc_sa"},
{"from": "170.80.0.0", "to": "170.83.255.254", "m": 14, "g": "misc_sa"},
{"from": "170.254.0.0", "to": "170.254.255.254", "m": 16, "g": "misc_sa"},
{"from": "171.224.0.0", "to": "171.239.255.254", "m": 12, "g": "viettel"},
{"from": "177.0.0.0", "to": "177.255.255.254", "m": 8, "g": "brasilnet"},
{"from": "178.156.128.0", "to": "178.156.255.254", "m": 17, "g": "hetzner"},
{"from": "179.0.0.0", "to": "179.255.255.254", "m": 8, "g": "brasilnet"},
{"from": "181.0.0.0", "to": "181.255.255.254", "m": 8, "g": "misc_sa"},
{"from": "183.87.32.0", "to": "183.87.63.254", "m": 19, "g": "huawei"},
{"from": "186.0.0.0", "to": "186.255.255.254", "m": 8, "g": "misc_sa"},
{"from": "187.0.0.0", "to": "187.255.255.254", "m": 8, "g": "misc_sa"},
{"from": "188.0.0.0", "to": "188.255.255.254", "m": 8, "g": "misc_sa"},
{"from": "189.0.0.0", "to": "189.255.255.254", "m": 8, "g": "misc_sa"},
{"from": "190.0.0.0", "to": "190.255.255.254", "m": 8, "g": "misc_sa"},
{"from": "191.0.0.0", "to": "191.255.255.254", "m": 8, "g": "misc_sa"},
{"from": "192.124.170.0", "to": "192.124.182.254", "g": "relcom"},
{"from": "200.0.0.0", "to": "200.255.255.254", "m": 8, "g": "misc_sa"},
{"from": "201.0.0.0", "to": "201.255.255.254", "m": 8, "g": "misc_sa"},
{"from": "212.95.128.0", "to": "212.95.159.254", "m": 19, "g": "asiacell"},
{"from": "222.252.0.0", "to": "222.252.255.254", "m": 14, "g": "vnpt"},
{"from": "2001:4860::::::", "to": "2001:4860:ffff:ffff:ffff:ffff:ffff:ffff", "m": 32, "g": "google"},
@@ -97,7 +88,6 @@
{"from": "2603:6010::::::", "to": "2603:6010:ffff:ffff:ffff:ffff:ffff:ffff", "m": 32, "g": "charter"},
{"from": "2603:8000::::::", "to": "2603:80ff:ffff:ffff:ffff:ffff:ffff:ffff", "m": 24, "g": "charter"},
{"from": "2607:a400::::::", "to": "2607:a400:ffff:ffff:ffff:ffff:ffff:ffff", "m": 32, "g": "zenlayer"},
{"from": "2804:::::::", "to": "2804:FFFF:FFFF:FFFF:FFFF:FFFF:FFFF:FFFF", "m": 16, "g": "misc_sa"},
{"from": "2a0a:4cc0::::::", "to": "2a0a:4cc0:FFFF:FFFF:FFFF:FFFF:FFFF:FFFF", "g": "netcup"}
]
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.9 KiB

After

Width:  |  Height:  |  Size: 2.1 KiB

139
img/busy-light.svg Normal file
View File

@@ -0,0 +1,139 @@
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 36 36">
<style>.box{fill:#00CADB;transform-origin: 50% 50%}
@keyframes box-1 {
9% {transform: translate(-12px,0)}
18% {transform: translate(0px,0)}
27% {transform: translate(0px,0)}
36% {transform: translate(12px,0)}
45% {transform: translate(12px,12px)}
55% {transform: translate(12px,12px)}
64% {transform: translate(12px,12px)}
73% {transform: translate(12px,0px)}
82% {transform: translate(0px,0px)}
91% {transform: translate(-12px,0px)}
100% {transform: translate(0px,0px)}
}
.box:nth-child(1){animation: box-1 4s infinite}
@keyframes box-2 {
9% {transform: translate(0,0)}
18% {transform: translate(12px,0)}
27% {transform: translate(0px,0)}
36% {transform: translate(12px,0)}
45% {transform: translate(12px,12px)}
55% {transform: translate(12px,12px)}
64% {transform: translate(12px,12px)}
73% {transform: translate(12px,12px)}
82% {transform: translate(0px,12px)}
91% {transform: translate(0px,12px)}
100% {transform: translate(0px,0px)}
}
.box:nth-child(2){animation: box-2 4s infinite}
@keyframes box-3 {
9% {transform: translate(-12px,0)}
18% {transform: translate(-12px,0)}
27% {transform: translate(0px,0)}
36% {transform: translate(-12px,0)}
45% {transform: translate(-12px,0)}
55% {transform: translate(-12px,0)}
64% {transform: translate(-12px,0)}
73% {transform: translate(-12px,0)}
82% {transform: translate(-12px,-12px)}
91% {transform: translate(0px,-12px)}
100% {transform: translate(0px,0px)}
}
.box:nth-child(3) {animation: box-3 4s infinite}
@keyframes box-4 {
9% {transform: translate(-12px,0)}
18% {transform: translate(-12px,0)}
27% {transform: translate(-12px,-12px)}
36% {transform: translate(0px,-12px)}
45% {transform: translate(0px,0px)}
55% {transform: translate(0px,-12px)}
64% {transform: translate(0px,-12px)}
73% {transform: translate(0px,-12px)}
82% {transform: translate(-12px,-12px)}
91% {transform: translate(-12px,0px)}
100% {transform: translate(0px,0px)}
}
.box:nth-child(4) {animation: box-4 4s infinite}
@keyframes box-5 {
9% {transform: translate(0,0)}
18% {transform: translate(0,0)}
27% {transform: translate(0,0)}
36% {transform: translate(12px,0)}
45% {transform: translate(12px,0)}
55% {transform: translate(12px,0)}
64% {transform: translate(12px,0)}
73% {transform: translate(12px,0)}
82% {transform: translate(12px,-12px)}
91% {transform: translate(0px,-12px)}
100% {transform: translate(0px,0px)}
}
.box:nth-child(5) {animation: box-5 4s infinite}
@keyframes box-6 {
9% {transform: translate(0,0)}
18% {transform: translate(-12px,0)}
27% {transform: translate(-12px,0)}
36% {transform: translate(0px,0)}
45% {transform: translate(0px,0)}
55% {transform: translate(0px,0)}
64% {transform: translate(0px,0)}
73% {transform: translate(0px,12px)}
82% {transform: translate(-12px,12px)}
91% {transform: translate(-12px,0px)}
100% {transform: translate(0px,0px)}
}
.box:nth-child(6) {animation: box-6 4s infinite}
@keyframes box-7 {
9% {transform: translate(12px,0)}
18% {transform: translate(12px,0)}
27% {transform: translate(12px,0)}
36% {transform: translate(0px,0)}
45% {transform: translate(0px,-12px)}
55% {transform: translate(12px,-12px)}
64% {transform: translate(0px,-12px)}
73% {transform: translate(0px,-12px)}
82% {transform: translate(0px,0px)}
91% {transform: translate(12px,0px)}
100% {transform: translate(0px,0px)}
}
.box:nth-child(7) {animation: box-7 4s infinite}
@keyframes box-8 {
9% {transform: translate(0,0)}
18% {transform: translate(-12px,0)}
27% {transform: translate(-12px,-12px)}
36% {transform: translate(0px,-12px)}
45% {transform: translate(0px,-12px)}
55% {transform: translate(0px,-12px)}
64% {transform: translate(0px,-12px)}
73% {transform: translate(0px,-12px)}
82% {transform: translate(12px,-12px)}
91% {transform: translate(12px,0px)}
100% {transform: translate(0px,0px)}
}
.box:nth-child(8){animation: box-8 4s infinite}
@keyframes box-9{
9% {transform: translate(-12px,0)}
18% {transform: translate(-12px,0)}
27% {transform: translate(0px,0)}
36% {transform: translate(-12px,0)}
45% {transform: translate(0px,0)}
55% {transform: translate(0px,0)}
64% {transform: translate(-12px,0)}
73% {transform: translate(-12px,0)}
82% {transform: translate(-24px,0)}
91% {transform: translate(-12px,0)}
100% {transform: translate(0px,0)}
}
.box:nth-child(9) {animation: box-9 4s infinite}
</style>
<g><rect class="box" x="13" y="1" rx="1" width="10" height="10"/>
<rect class="box" x="13" y="1" rx="1" width="10" height="10"/>
<rect class="box" x="25" y="25" rx="1" width="10" height="10"/>
<rect class="box" x="13" y="13" rx="1" width="10" height="10"/>
<rect class="box" x="13" y="13" rx="1" width="10" height="10"/>
<rect class="box" x="25" y="13" rx="1" width="10" height="10"/>
<rect class="box" x="1" y="25" rx="1" width="10" height="10"/>
<rect class="box" x="13" y="25" rx="1" width="10" height="10"/>
<rect class="box" x="25" y="25" rx="1" width="10" height="10"/></g>
</svg>

After

Width:  |  Height:  |  Size: 4.9 KiB

BIN
img/captcha.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 23 KiB

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.8 KiB

After

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.4 KiB

After

Width:  |  Height:  |  Size: 4.1 KiB

1
img/views.svg Normal file
View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><title>eye-outline</title><path d="M12,9A3,3 0 0,1 15,12A3,3 0 0,1 12,15A3,3 0 0,1 9,12A3,3 0 0,1 12,9M12,4.5C17,4.5 21.27,7.61 23,12C21.27,16.39 17,19.5 12,19.5C7,19.5 2.73,16.39 1,12C2.73,7.61 7,4.5 12,4.5M3.18,12C4.83,15.36 8.24,17.5 12,17.5C15.76,17.5 19.17,15.36 20.82,12C19.17,8.64 15.76,6.5 12,6.5C8.24,6.5 4.83,8.64 3.18,12Z" style="fill:#777"/></svg>

After

Width:  |  Height:  |  Size: 419 B

15
lang/de/lang.php Normal file
View File

@@ -0,0 +1,15 @@
<?php
/**
* German language file for BotMon plugin
*
* @author Sascha Leib <ad@hominem.info>
*/
// Captcha dialog locale strings:
$lang['bm_dlgTitle'] = 'Benutzer&uuml;berpr&uuml;fung';
$lang['bm_dlgSubtitle'] = 'Bitte best&auml;tige, dass du kein Bot bist:';
$lang['bm_dlgConfirm'] = 'Klicke, um zu best&auml;tigen.';
$lang['bm_dlgChecking'] = 'Wird &uuml;berpr&uuml;ft&nbsp;&hellip;';
$lang['bm_dlgLoading'] = 'Seite wird geladen&nbsp;&hellip;';
$lang['bm_dlgError'] = 'Es ist ein Fehler aufgetreten.';
$lang['bm_noJsWarning'] = 'Bitte aktivieren Sie JavaScript, um diese Seite anzuzeigen.';

2734
lang/de/wordlist.txt Normal file

File diff suppressed because it is too large Load Diff

15
lang/en/lang.php Normal file
View File

@@ -0,0 +1,15 @@
<?php
/**
* English language file for BotMon plugin
*
* @author Sascha Leib <ad@hominem.info>
*/
// Captcha dialog locale strings:
$lang['bm_dlgTitle'] = 'User Check';
$lang['bm_dlgSubtitle'] = 'Please confirm you are not a bot:';
$lang['bm_dlgConfirm'] = 'Click to confirm.';
$lang['bm_dlgChecking'] = 'Checking&nbsp;&hellip;';
$lang['bm_dlgLoading'] = 'Loading page&nbsp;&hellip;';
$lang['bm_dlgError'] = 'An error occured.';
$lang['bm_noJsWarning'] = 'This page requires JavaScript to be enabled.';

View File

@@ -5,6 +5,22 @@
* @author Sascha Leib <sascha@leib.be>
*/
$lang['showday'] = 'Which data to show in the “Latest” tab:';
$lang['showday_o_yesterday'] = 'Last full day (yesterday)';
$lang['showday_o_today'] = 'Ongoing logs (today)';
$lang['combineNets'] = 'Combine visits from known IP-ranges into one entry:';
$lang['geoiplib'] = 'Add GeoIP Information<br><small>(requires PHP module to be installed)</small>';
$lang['geoiplib_o_disabled'] = 'Disabled';
$lang['geoiplib_o_phpgeoip'] = 'Use GeoIP Module';
$lang['geoiplib_o_disabled'] = 'Disabled';
$lang['geoiplib_o_phpgeoip'] = 'Use GeoIP Module';
$lang['useCaptcha'] = 'Enable Captcha<br><small>(Experimental, read the manual first!)</small>';
$lang['useCaptcha_o_disabled'] = 'Disabled';
$lang['useCaptcha_o_loremipsum'] = 'Captcha with Lorem ipsum text';
$lang['useCaptcha_o_dada'] = 'Captcha with Dada placeholder text';
$lang['captchaSeed'] = 'Captcha Seed<br><small>(Enter a random string, e.g. <a href="https://www.browserling.com/tools/random-hex" target="_blank">from here</a>)</small>';
$lang['captchaBypass'] = 'Automatically solve the Captcha, if …<br><small>(Note: this probably does not make much sense for English wikis!)</small>';
$lang['captchaBypass_langmatch'] = 'Client and page languages match ';

3050
lang/en/wordlist.txt Normal file

File diff suppressed because it is too large Load Diff

9
lang/fr/lang.php Normal file
View File

@@ -0,0 +1,9 @@
<?php
/**
* French language file for BotMon plugin
*
* @author Sascha Leib <ad@hominem.info>
*/
// Captcha dialog locale strings:
$lang['bm_noJsWarning'] = 'Veuillez activer JavaScript pour afficher cette page.';

3333
lang/fr/wordlist.txt Normal file

File diff suppressed because it is too large Load Diff

1577
lang/la/wordlist.txt Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -1,7 +1,7 @@
base botmon
author Sascha Leib
email ad@hominem.com
date 2025-10-20
date 2025-12-06
name Bot Monitoring
desc A tool for monitoring and analysing bot traffic to your wiki (under development)
desc A tool for monitoring and blocking bot traffic to your wiki (under development)
url https://www.dokuwiki.org/plugin:botmon

View File

@@ -1 +1,164 @@
/* This file is no longer in use */
body.botmon_captcha {
main {
h1 {
color: transparent !important;
text-shadow: 0 0 .25em rgba(0,.0,0,.8);
}
p, dt, dd, h2, h3, h4, h5, h6, a:link, a:visited {
color: transparent !important;
text-shadow: 0 0 .35em rgba(0,0,0,.5);
user-select: none;
}
svg {
width: auto; height: auto;
-webkit-filter: blur(8px); /* For Safari */
filter: blur(8px);
}
}
#botmon_captcha_box {
& {
position: fixed;
width: 400px;
height: 220px;
top: ~"calc(50vh - 110px)";
left: ~"calc(50vw - 200px)";
background: #1C1B22 none;
border: #7C7B82 solid 1pt;
border-radius: 12px;
margin: 0; padding: 18px;
font-family: system-ui, sans-serif;
box-shadow: .25rem .25rem .5rem rgba(0,0,0,.5);
box-sizing: border-box;
z-index: 10001;
}
& * {
color: #EDEDF5;
margin: 0;
}
h2 {
font-size: 24px;
line-height: 32px;
padding: 0 0 12px 0;
}
p {
font-size: 16px;
line-height: 20px;
padding: 6px 0;
}
label {
& {
display: grid;
grid-template-columns: 32px auto;
column-gap: 8px;
align-items: center;
padding: 24px;
margin: 16px 0 0 0;
background-color: #32313A;
border: #7C7B82 solid 1px;
border-radius: 3px;
font-size: 16px;
}
span.busy {
display: inline-block;
width: 24px; height: 24px;
background: transparent url('img/busy-light.svg') center no-repeat;
background-size: 24px;
}
span.error, span.erricon {
color: #f96c6c;
}
}
input[type="checkbox"] {
width: 24px; height: 24px;
border: 2px solid #00CADB;
border-radius: 4px;
appearance: none;
}
input[type="checkbox"]:focus {
outline: none;
box-shadow: 0 0 6px rgba(0, 202, 219, .8);
}
input[type="checkbox"]:disabled {
display: none;
}
&.checking {
input[type="checkbox"], span.confirm, span.loading, span.erricon, span.error { display: none;}
span.busy, span.checking { display: initial; }
label, input[type="checkbox"] { cursor: wait; }
}
&.ready {
input[type="checkbox"], span.confirm { display: initial;}
span.busy, span.checking, span.loading, span.erricon, span.error { display: none; }
label, input[type="checkbox"] { cursor: pointer; }
}
&.loading {
span.busy, span.loading { display: initial; }
input[type="checkbox"], span.confirm, span.checking, span.erricon, span.error { display: none;}
label { cursor: wait; }
}
&.error {
span.erricon, span.error { display: initial; }
input[type="checkbox"], span.confirm, span.checking, span.busy, span.loading { display: none;}
label { cursor: initial; }
}
}
}
@keyframes delayed-fade-in {
0% { opacity: 0; }
50% { opacity: 0; }
100% { opacity: 1; }
}
// no js warning
#BM__NoJSWarning {
position: fixed;
bottom: calc(50vh - 2.5rem);
width: 100%; max-width: fit-content;
margin: 0 auto;
padding: .25rem 1rem;
border-radius: .5rem;
border: red solid 2pt;
box-shadow: rgba(128, 0, 0, 0.5) .25rem .25rem .5rem;
animation: delayed-fade-in 4s forwards;
}
// captcha on smaller screens:
@media (max-width: 480px) {
body.botmon_captcha #botmon_captcha_box {
width: 100vw;
height: auto;
left: 0; top: 80px;
border-radius: 2px;
}
}
/* dark mode overrides */
@media (prefers-color-scheme: dark) {
body.darkmode.botmon_captcha {
main {
h1 {
text-shadow: 0 0 .25em rgba(170,170,170,.75);
}
p, h2, h3, h4, h5, h6 {
text-shadow: 0 0 .35em rgba(170,170,170,.75);
}
}
#botmon_captcha_box {
& {
background-color: #FFF;
border-color: #9F9EA1;
box-shadow: .25rem .25rem .5rem rgba(0,0,0,.25);
}
& * {
color: #15141A;
}
label {
background-color: #EEE;
border-color: #9F9EA1;
}
}
}
}