Esses dias eu vi um post no Mastodon sobre bloquear robôs que acessam o servidor web.
Perguntei pro autor do post como foi que ele conseguiu isso, mas fui solenemente ignorado. Coisas da Internet.
Então resolvi dar uma olhada nos logs do servidor, esse que hospeda esse mesmo site. E fiz um programa em perl pra isso. Pra matar as saudades. E tirar a ferrugem.
E esse foi o programa:
#! /usr/bin/env perl
use IO::Zlib;
my $LOGDIR = "/var/log/apache2";
opendir(DIR, $LOGDIR) or die "Impossible to read from directory: $!\n";
%ip_addrs;
%bot_agent;
@gzip_files;
foreach my $filename (readdir DIR) {
next if $filename !~ /access/;
# skip gz right now
if ($filename =~ /\.gz/) {
push(@gzip_files, ($LOGDIR."/".$filename));
next;
}
print($LOGDIR."/".$filename."\n");
open(FD, $LOGDIR."/".$filename) or die "Impossible to read file: $!\n";
foreach my $line () {
next if $line !~ /bot/;
parse_log_line($line);
}
}
print("result:\n");
for my $filename (@gzip_files) {
print("$filename\n");
my $fh = new IO::Zlib;
$fh->open($filename, "rb") or die "impossible to read gzip file: $!\n";
while ( my $line = <$fh>) {
next if $line !~ /bot/;
parse_log_line($line);
}
}
foreach $bot (sort {$bot_agent{$b}<=>$bot_agent{$a} } keys %bot_agent) {
print("$bot => $bot_agent{$bot}\n");
}
sub parse_log_line() {
my $line = $_[0];
our %ip_addrs;
our %bot_agent;
@params = split(/ /, $line);
my $ip = $params[0];
$ip_addrs{$ip}++;
$line =~ s/.*]//;
$line =~ s/\"$//;
$line =~s/.*\"//;
chomp($line);
if ($line =~ m/bot/) {
$bot_agent{$line}++;
}
}
E o resultado em formato de tabela:
User-Agent do robô | Quantidade de acessos |
---|---|
Mozilla/5.0 (compatible; MJ12bot/v1.4.8; http://mj12bot.com/) | 30130 |
Mozilla/5.0 (compatible; SemrushBot/7~bl; +http://www.semrush.com/bot.html) | 22203 |
Mozilla/5.0 AppleWebKit/537.36 (KHTML, like Gecko; compatible; ClaudeBot/1.0; | 16981 |
Mozilla/5.0 AppleWebKit/537.36 (KHTML, like Gecko; compatible; bingbot/2.0; +http://www.bing.com/bingbot.htm) Chrome/116.0.1938.76 Safari/537.36 | 15979 |
Mozilla/5.0 (compatible; AhrefsBot/7.0; +http://ahrefs.com/robot/) | 12112 |
Mozilla/5.0 (Linux; Android 7.0;) AppleWebKit/537.36 (KHTML, like Gecko) Mobile Safari/537.36 (compatible; PetalBot;+https://webmaster.petalsearch.com/site/petalbot) | 10744 |
Mozilla/5.0 AppleWebKit/537.36 (KHTML, like Gecko; compatible; Amazonbot/0.1; +https://developer.amazon.com/support/amazonbot) Chrome/119.0.6045.214 Safari/537.36 | 9704 |
Mozilla/5.0 (compatible; DotBot/1.2; +https://opensiteexplorer.org/dotbot; | 7434 |
Mozilla/5.0 (compatible; AwarioBot/1.0; +https://awario.com/bots.html) | 5042 |
Mozilla/5.0 (compatible; DataForSeoBot/1.0; +https://dataforseo.com/dataforseo-bot) | 3104 |
Linguee Bot (http://www.linguee.com/bot; | 1957 |
Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.4 Safari/605.1.15 (Applebot/0.1; +http://www.apple.com/go/applebot) | 1450 |
Mozilla/5.0 (compatible; archive.org_bot +http://archive.org/details/archive.org_bot) Zeno/5741de8 warc/v0.8.85 | 751 |
Mozilla/5.0 (compatible; SemrushBot-BA; +http://www.semrush.com/bot.html) | 651 |
Mozilla/5.0 (compatible; YandexBot/3.0; +http://yandex.com/bots) | 567 |
Mozilla/5.0 (compatible; MJ12bot/v2.0.2; http://mj12bot.com/) | 538 |
Mozilla/5.0 (Linux; Android 6.0.1; Nexus 5X Build/MMB29P) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.7204.183 Mobile Safari/537.36 (compatible; Googlebot/2.1; +http://www.google.com/bot.html) | 531 |
Mozilla/5.0 (compatible; Googlebot/2.1; +http://www.google.com/bot.html) | 411 |
Blogtrottr/2.1 (+https://blogtrottr.com/robot) | 340 |
Googlebot-Image/1.0 | 315 |
Mozilla/5.0 AppleWebKit/537.36 (KHTML, like Gecko; compatible; bingbot/2.0; +http://www.bing.com/bingbot.htm) Chrome/100.0.4896.127 Safari/537.36 | 300 |
Mozilla/5.0 AppleWebKit/537.36 (KHTML, like Gecko; compatible; GPTBot/1.2; +https://openai.com/gptbot) | 261 |
Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36; compatible; OAI-SearchBot/1.0; +https://openai.com/searchbot | 240 |
Mozilla/5.0 (Linux; Android 6.0.1; Nexus 5X Build/MMB29P) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/137.0.7151.119 Mobile Safari/537.36 (compatible; Googlebot/2.1; +http://www.google.com/bot.html) | 207 |
Mozilla/5.0 AppleWebKit/537.36 (KHTML, like Gecko; compatible; bingbot/2.0; +http://www.bing.com/bingbot.htm) Chrome/136.0.0.0 Safari/537.36 | 191 |
ZoominfoBot (zoominfobot at zoominfo dot com) | 164 |
Mozilla/5.0 AppleWebKit/537.36 (KHTML, like Gecko); compatible; ChatGPT-User/1.0; +https://openai.com/bot | 150 |
Mozilla/5.0 (compatible; SeznamBot/4.0; +https://o-seznam.cz/napoveda/vyhledavani/en/seznambot-crawler/) | 124 |
Mozilla/5.0 (compatible; MojeekBot/0.11; +https://www.mojeek.com/bot.html) | 107 |
Mozilla/5.0 (compatible; archive.org_bot +http://archive.org/details/archive.org_bot) Zeno/a7797cb warc/v0.8.78 | 89 |
DuckDuckBot/1.1; (+http://duckduckgo.com/duckduckbot.html) | 82 |
Mozilla/5.0 (compatible;PetalBot;+https://webmaster.petalsearch.com/site/petalbot) | 80 |
Mozilla/5.0 (compatible; YandexImages/3.0; +http://yandex.com/bots) | 70 |
Mozilla/5.0 AppleWebKit/537.36 (KHTML, like Gecko; compatible; PerplexityBot/1.0; +https://perplexity.ai/perplexitybot) | 62 |
Mozilla/5.0 (compatible; YaK/1.0; http://linkfluence.com/; | 61 |
Mozilla/5.0 (compatible; coccocbot-image/1.0; +http://help.coccoc.com/searchengine) | 56 |
Mozilla/5.0 (compatible; wpbot/1.3; +https://forms.gle/ajBaxygz9jSR8p8G9) | 41 |
Mozilla/5.0 AppleWebKit/537.36 (KHTML, like Gecko; compatible; Googlebot/2.1; +http://www.google.com/bot.html) Chrome/137.0.7151.119 Safari/537.36 | 37 |
Mozilla/5.0 (Linux; Android 6.0.1; Nexus 5X Build/MMB29P) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.7204.168 Mobile Safari/537.36 (compatible; Googlebot/2.1; +http://www.google.com/bot.html) | 29 |
Twitterbot/1.0 | 29 |
AdsBot-Google (+http://www.google.com/adsbot.html) | 22 |
Mozilla/5.0 (compatible; intelx.io_bot +https://intelx.io) | 21 |
BufferLinkPreviewBot/1.0 (+https://scraper.buffer.com/about/bots/link-preview-bot) | 19 |
Mozilla/5.0 (Linux; Android 6.0.1; Nexus 5X Build/MMB29P) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/99.0.4844.84 Mobile Safari/537.36 (compatible; Googlebot/2.1; +http://www.google.com/bot.html) | 14 |
Mozilla/5.0 (compatible; Thinkbot/0.5.8; +In_the_test_phase,_if_the_Thinkbot_brings_you_trouble,_please_block_its_IP_address._Thank_you.) | 13 |
Mozilla/5.0 (compatible; MJ12bot/v2.0.4; http://mj12bot.com/) | 13 |
Mozilla/5.0 (Linux; Android 6.0.1; Nexus 5X Build/MMB29P) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.7204.183 Mobile Safari/537.36 (compatible; AdsBot-Google-Mobile; +http://www.google.com/mobile/adsbot.html) | 9 |
Mozilla/4.0 (compatible; fluid/0.0; +http://www.leak.info/bot.html) | 8 |
Mozilla/5.0 (Windows NT 10.0; Win64; x64; trendictionbot0.5.0; trendiction search; http://www.trendiction.de/bot; please let us know of any problems; web at trendiction.com) Gecko/20100101 Firefox/125.0 | 7 |
Googlebot/2.1 (+http://www.google.com/bot.html) | 5 |
Mozilla/5.0 (compatible; SurdotlyBot/1.0; +http://sur.ly/bot.html) | 5 |
Mozilla/5.0 (compatible; Website-info.net-Robot; https://website-info.net/robot) | 4 |
Mozilla/5.0 (compatible; coccocbot-web/1.0; +http://help.coccoc.com/searchengine) | 4 |
Pandalytics/2.0 (https://domainsbot.com/pandalytics/) | 4 |
Mozilla/5.0 (compatible; IbouBot/1.0; | 4 |
DomainStatsBot/1.0 (https://domainstats.com/pages/our-bot) | 4 |
Mozilla/5.0 (Linux; Android 6.0.1; Nexus 5X Build/MMB29P) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/W.X.Y.Z Mobile Safari/537.36 (compatible; Googlebot/2.1; +http://www.google.com/bot.html) | 3 |
Mozilla/5.0 (compatible; SeekportBot; +https://bot.seekport.com) | 3 |
serpstatbot/2.1 (advanced backlink tracking bot; https://serpstatbot.com/; | 3 |
Mozilla/5.0 (Macintosh; Intel Mac OS X 10_11_1) AppleWebKit/601.2.4 (KHTML, like Gecko) Version/9.0.1 Safari/601.2.4 facebookexternalhit/1.1 Facebot Twitterbot/1.0 | 3 |
yacybot (-global; amd64 Linux 5.15.161; java 11.0.26-internal; America/en) http://yacy.net/bot.html | 2 |
Mozilla/5.0 AppleWebKit/537.36 (KHTML, like Gecko; compatible; Googlebot/2.1; +http://www.google.com/bot.html) Chrome/120.0.6099.199 Safari/537.36 | 2 |
Mozilla/5.0 (compatible; Qwantbot/1.0_4396629; +https://help.qwant.com/bot/) | 2 |
DomCopBot (https://www.domcop.com/bot) | 2 |
Mozilla/5.0 (compatible; YandexFavicons/1.0; +http://yandex.com/bots) | 2 |
yacybot (/global; amd64 Linux 5.15.0-140-generic; java 11.0.27; America/en) http://yacy.net/bot.html | 2 |
Synapse (bot; +https://github.com/matrix-org/synapse) | 2 |
Mozilla/5.0 (compatible; archive.org_bot; Wayback Machine Live Record; +http://archive.org/details/archive.org_bot) | 1 |
Mozilla/5.0 (Linux; Android 6.0.1; Nexus 5X Build/MMB29P) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Mobile Safari/537.36 (compatible; Googlebot/2.1; +http://www.google.com/bot.html) | 1 |
Mozilla/5.0 (compatible; Qwantbot/1.0; +https://help.qwant.com/bot/) | 1 |
Facebot | 1 |
Mozilla/5.0 (compatible; GetHPinfo.com-Bot/0.1; +http://www.gethpinfo.com/bot/ | 1 |
Slack-ImgProxy (+https://api.slack.com/robots) | 1 |
Googlebot-Video/1.0 | 1 |
yacybot (/global; amd64 Linux 6.12.38+deb13-amd64; java 21.0.8; Europe/fr) http://yacy.net/bot.html | 1 |
Mozilla/5.0 (iPhone; CPU iPhone OS 6_0 like Mac OS X) AppleWebKit/536.26 (KHTML, like Gecko) Version/6.0 Mobile/10A5376e Safari/8536.25 (compatible; SMTBot/1.0; +http://www.similartech.com/smtbot) | 1 |
Mastodon/4.5.0-alpha.1+chuckya (http.rb/5.3.1; +https://lab.wheelsbot.dev/) | 1 |
O resultado é texto. Eu só formatei pra ficar mais fácil de visualizar aqui (usando "sd" pra isso). E está hardcoded pra buscar os logs do apache2 em sistemas debian alike.
Dos resultados, confesso que fiquei surpreso. Realmente bastante tráfego vindo de robôs. E vários que eu nunca ouvi falar.
Sobre bloquear ou não, eu por enquanto não mexi em nada e os robôs continuam acessando tudo. Mesmo porque eu não fiz nada qualitativo, pra saber se estão recebendo 200 (ok) ou alguma outra coisa como 404 (not found - não encontrad).
Mas caso eu decida pelo bloqueio, achei um projeto bem interessante no GitHub que já faz a curadoria de robôs "bons" e "ruins".
Se assim como eu, você precisa invariavelmente entregar programas binários e sem o código fonte, então também está sentindo falta do perlcc, que compilava o programa em perl em um belo binário.
Devido à vários bugs, incompatibilidades e falta de manutenção, o perlcc foi removido já no perl 5.8.
Eu testei várias outras soluções, mas ultimamente estava entregando binário utilizando o freeze do python, que eu achava mais simples.
Hoje, buscando uma outra coisa perl, topei com um PAR packer. Tanto a sintaxe quanto a funcionalidade é idêntica ao perlcc.
Eu testei aqui, e funcionou muito bem.
helio@shibboleet:tmp$ pp -B -o accumulatorEventSender accumulatorEventSender.pl helio@shibboleet:tmp$ ls accumulatorEventSender accumulatorEventSender.pl helio@shibboleet:tmp$ file accumulatorEventSender accumulatorEventSender: ELF 32-bit LSB executable, Intel 80386, version 1 (SYSV),
dynamically linked (uses shared libs), for GNU/Linux 2.6.15,
BuildID[sha1]=0x75828a5e4b38f0a935c9461e07d6ac23d3282d73, stripped
O aplicativo se encontra no pacote libpar-packer-perl no Ubuntu (e possivelmente no Debian).
No sistema de agendamento de tarefas, o cron, existe a possibilidade de controle de acesso pelos arquivos /etc/cron.allow e /etc/cron.deny. Existindo somente o /etc/cron.allow no sistema, o usuário deverá ter seu login incluso nesse arquivo para fazer uso do cron. Do contrário será brindando com uma mensagem semelhante a essa:
You (helio) are not allowed to use this program (crontab)
See crontab(1) for more information
Nos ambientes de telecomunicações, onde muitos dos sistemas operacionais em uso são Solaris, ex-Sun e agora da Oracle, esses vêm com essa forma de controle do cron habilitada por padrão.
Isso aumenta a segurança do sistema, mas também torna alguns trabalhos simples mais difíceis, principalmente quando é necessário ter agendamento de trabalho. Claro que isso poderia ser resolvido com ajuda de um bom administrador de sistemas, o sysadmin, e conhecimento em Unix, mas isso parece estar se tornando cada vez mais raro nesses dias.
Então criei o mycrond, um daemon escrito em perl, para rodar periodicamente como o crond do sistema. A sintaxe não é perfeita e tem alguns furos, como não aceitar "*/5" para tarefas a cada 5 intervalos, mas utiliza uma sintaxe que somente Solaris aceita como, por exemplo para tarefas agendadas para cada 5 minutos, utilizar "0,5,10,15,20,25,30,35,40,45,50,55". O restante é parecido com o uso normal da crontab.
Não é 100% perfeito, pois acabei deixando a análise de sintaxe incompleta para todos os valores de data, mas funciona perfeitamente como daemon, não morrendo ao sair do sistema, e é capaz de analisar e rodar formas mais simples da crontab.
O conteúdo do programa é o seguinte:
#! /usr/bin/perl
# $Id: mycrond 29 2011-06-22 20:42:35Z helio $
# Date: Wed Jun 22 12:37:00 BRT 2011
# Author: Helio Loureiro
# Description: an user level alternative to system crond.
#
my $ROOTDIR = "/home/helio/AUDIT";
exit(0); # Edit $ROOTDIR settings and comment this exit.
my $CRONFILE = $ROOTDIR."/etc/crontab";
my $PIDFILE = $ROOTDIR."/var/run/mycrond.pid";
$SIG{CHLD} = 'IGNORE'; #avoid zombies, or they can eat your brain
### Starting in daemon mode
&Daemonize();
sub CheckDir() {
my $dir = $_[0];
if ( ! -d "$dir" ) {
system("mkdir -p $dir");
}
}
sub ParseCronLine() {
my $cron = $_[0];
chomp($cron);
my @params = split(/ /, $cron);
my $r_minute = $params[0];
my $r_hour = $params[1];
my $r_day = $params[2];
my $r_month = $params[3];
my $r_dow = $params[4];
my $command = $params[5];
for (my $x = 6; $x < @params; $x++) {
$command .= " ".$params[$x];
}
my ($second, $minute, $hour, $dayOfMonth, $month, $year, $dayOfWeek,
$dayOfYear, $daylightSavings) = localtime();
$year += 1900;
$month++;
### Day of week requires some more work
if ($r_dow != '*') {
if ($r_dow != $dayOfWeek) {
return 0;
}
}
if ($r_month != '*') {
$run_status = 0;
@months = split(/,/, $r_month);
foreach $m (@months) {
if ($m == $month) {
$run_status++ ;
}
}
return if (! $run_status);
}
if ($r_day != '*') {
$run_status = 0;
@days = split(/,/, $r_day);
foreach $d (@days) {
if ($d == $dayOfMonth) {
$run_status++ ;
}
}
return if (! $run_status);
}
if ($r_minute != '*') {
$run_status = 0;
@minutes = split(/,/, $r_minute);
foreach $m (@minutes) {
if ($m == $minute) {
$run_status++ ;
}
}
return if (! $run_status);
}
if ($r_hour != '*') {
$run_status = 0;
@hours = split(/,/, $r_hour);
foreach $h (@hours) {
if ($h == $hour) {
$run_status++ ;
}
}
return if (! $run_status);
}
# If it is ok, then run your command
if (defined($DEBUG)){
print "Running: $command\n";
}
exec "$command" or exit(1);
exit(0);
}
sub Run() {
### Saving process PID
my $pid = getppid();
$pid++;
&CheckDir($ROOTDIR."/var/run");
open(PID, ">$PIDFILE") or die "Impossible to create $PIDFILE: $!\n";
print PID $pid."\n";
close(PID);
open(FD, $CRONFILE) or die "Impossible to read crontab: $!\n";
my @cronlines = ;
close(FD);
my @PIDS;
while (1) {
foreach my $line (@cronlines) {
next if ($line =~ m/^#/);
my $runpid = fork();
if ($runpid == 0) {
&ParseCronLine($line);
exit(0);
}
if ($runpid > 0) {
push(@PIDS, $runpid);
}
}
sleep(60);
}
}
sub Daemonize() {
# Change to /
chdir("/");
my $pid = fork(); #first fork
if ($pid > 0) { #Parent exists
exit(0);
}
if ($pid == 0) { # Son forks again
my $pid2 = fork();
if ($pid2 > 0) {
exit(0);
}
if ($pid2 == 0) {
&Run();
}
}
}
Faz tempo que não me deparo com esse tipo de problema, mas acabei vendo novamente depois que um colega de trabalho pediu ajuda nisso.
A primeira vez que trabalhei com conversão de caracteres foi em português mesmo, de ISO-8859-1 para UTF-8. Não guardei lembranças boas. Dessa vez o desafio era para converter supostos caracteres no mesmo formato para UTF-8, mas não da nossa língua pátria.
Para quem nunca viu isso, o código em Perl que faz a conversão está no pacote Encode , disponível no CPAN, da seguinte forma:
use Encode;
...
...
foreach $line () {
next if ($line =~ m/^$/);
chomp($line);
$i = Encode::decode("iso-8859-1", $line);
$line = Encode::encode("utf8", $i);
print $line."\n";
}
...
Dessa vez tive um problema com a palavra "ΑστέριαΑστέρια". Juro que não sei o que significa, mas a droga da palavra, escrita em grego (CP1253) estava junto ao restante do texto codificado em ISO-8859-1.
Apanhei pra achar o formato certo... ainda falta fazer um conversor inteligente o suficiente para usar o formato certo antes de processar, mas já é alguma coisa...