acck/index.php
2024-08-27 20:00:56 +02:00

219 lines
11 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<?php
/*
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
require 'lib/validhostname.php';
require 'lib/ckratelimit.php';
require 'lib/ght.php';
const SNAME='acck';
const SVERS='0.1.7';
const SREPO='https://git.lattuga.net/jones/acck';
const MAXACCLEN=30+253+2;
const MAXHOSTLEN=253;
const INIFP='sec/conf.ini';
$usname=ucfirst(SNAME);
$timeout=5;
$cjr=rand(0,999999);
header('Content-Language: en');
if (file_exists(INIFP)) {
$conf=@parse_ini_file(INIFP,false,INI_SCANNER_RAW);
(isset($conf['use_proxy_with'])) ? $conf['use_proxy_with']=explode(',',$conf['use_proxy_with']) : $conf['use_proxy_with']=null;
if ($conf===false)
die('Configuration file «'.INIFP."» exists but {$usname} could not open it.");
}
$errors=[];
if (isset($_GET['acctock'])) {
if (strlen($_GET['acctock'])>MAXACCLEN) {
$_GET['acctock']='';
$errors[]='Value for «Fediverse account address» is too long';
}
$_GET['acctock']=trim($_GET['acctock']);
if ($_GET['acctock']!='' && preg_match('#^@?[0-9a-zA-Z_]+(@[a-z0-9.-]{4,253})?$#',$_GET['acctock'])!==1)// todo: make it better, like split ecc.
$errors[]='Value for «Fediverse account address» is not valid';
} else {
$_GET['acctock']='';
}
$hostok=false;
if (isset($_GET['host'])) {
if (strlen($_GET['host'])>MAXHOSTLEN) {
$_GET['host']='';
$errors[]='Value for «Mastodon instance domain» is too long';
}
$_GET['host']=trim($_GET['host']);
if ($_GET['host']!='' && !validhostname($_GET['host']))
$errors[]='Value for «Mastodon instance domain» is not valid';
else
$hostok=true;
} else {
$_GET['host']='';
}
$rl=['remaining'=>400,'restime'=>0];
if ($hostok) {
$rlfp='sec/'.$_GET['host'].'.rl.state';
if (file_exists($rlfp)) {
$buf=@file($rlfp,FILE_IGNORE_NEW_LINES);
if ($buf===false)
die('Could not open rate limiting state file.');
if (count($buf)!=2 || preg_match('#^\d+$#',$buf[0])!==1 || preg_match('#^\d+$#',$buf[1])!==1)
die('Malformed rate limiting state file.');
$rl=['remaining'=>$buf[0]+0,'restime'=>$buf[1]+0];
}
}
$now=time();
if ($rl['remaining']<=10 && $now<=$rl['restime'])// ten to leave a margin for "many people using it" and to account for 3 calls to host's endpoints
$errors[]="This {$usname} instance has reached rate limit on «{$_GET['host']}», please wait at least ".ght($rl['restime']-$now,null,0).'.';
if (count($errors)>0)
$errors='<div class="warning">There are some errors<ul><li>'.implode('</li><li>',$errors).'</li></ul></div><div class="horsep"></div>';
else
$errors='';
echo "<!DOCTYPE HTML>
<html lang=\"en\">
<head>
<title>{$usname}</title>
<meta http-equiv=\"Content-Type\" content=\"text/html; charset=utf-8\">
<meta name=\"description\" content=\"A tool to check if a fediverse account is moderated by a Mastodon instance\">
<meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no\">
<meta property=\"og:image\" content=\"imgs/ogimage.png\">
<link rel=\"icon\" type=\"image/png\" href=\"imgs/icon-16.png\" sizes=\"16x16\">
<link rel=\"icon\" type=\"image/png\" href=\"imgs/icon-32.png\" sizes=\"32x32\">
<link rel=\"icon\" type=\"image/png\" href=\"imgs/icon-192.png\" sizes=\"192x192\">
<link rel=\"icon\" type=\"image/png\" href=\"imgs/icon-512.png\" sizes=\"512x512\">
<link rel=\"apple-touch-icon-precomposed\" href=\"imgs/icon-180.png\">
<link rel=\"stylesheet\" type=\"text/css\" href=\"css/main.css?v={$cjr}\">
</head>
<body>
<div id=\"main\">
<h1>{$usname}</h1>
<div class=\"normtext\">
<p class=\"firstp\">Hello, this is {$usname}, a tool to easily check if a fediverse account is <a href=\"https://docs.joinmastodon.org/admin/moderation/#limit-user\">limited</a> (AKA «silenced») and-or <a href=\"https://docs.joinmastodon.org/admin/moderation/#suspend-user\">suspended</a> (AKA «blocked») by a Mastodon instance.</p>
<p>Since an account is reported as limited and-or suspended by a Mastodon instance even when that instance has limited and-or suspended the whole accounts instance, {$usname} also tries to detect if this is the case, and tells about it.</p>
<!-- <p>To check if an account is moderated by the given server, {$usname} tries to use the servers <a href=\"https://docs.joinmastodon.org/methods/accounts/#lookup\">«/api/v1/accounts/lookup» API endpoint</a>, that is public and was introduced in Mastodon v3.4.0; to check if the server is moderating the whole accounts domain, {$usname} tries to use the servers <a href=\"https://docs.joinmastodon.org/methods/instance/#domain_blocks\">«/api/v1/instance/domain_blocks» API endpoint</a>, that is not always set to public by the servers admins, and was introduced in Mastodon v4.0.0. If it cant use one or both of these endpoints, it tells.</p> -->
<p>{$usname} does not use cookies or Javascript and does not store any data about you anywhere.</p>
<p>You can find {$usname}s code <a href=\"".SREPO."\">here</a>, and you can contact its developer on Mastodon <a href=\"https://puntarella.party/@j0nes\">here</a>.</p>
</div>
".$errors."
<form method=\"get\" id=\"mainform\" name=\"mainform\">
<div class=\"inputdiv\"><label for=\"acctock\">Fediverse account address</label><input type=\"text\" id=\"acctock\" name=\"acctock\" class=\"input\" placeholder=\"Example: DonaldDuck@duckburg.social\" value=\"{$_GET['acctock']}\" maxlength=\"".MAXACCLEN."\" required></div>
<div class=\"inputdiv\"><label for=\"host\">Mastodon instance domain</label><input type=\"text\" id=\"host\" name=\"host\" class=\"input\" placeholder=\"Example: mastodon.social\" value=\"{$_GET['host']}\" maxlength=\"".MAXHOSTLEN."\" required></div>
<div class=\"lastinputdiv\"><button type=\"submit\" id=\"button\" class=\"button\">Check</button></div>\n</form>\n";
if ($errors=='' && $_GET['acctock']!='' && $_GET['host']!='') {
echo "<div class=\"horsep\"></div>\n";
$context=[
'http'=>[
'method'=>'GET',
'ignore_errors'=>true,
'protocol_version'=>1.1,
'user_agent'=>'Mozilla/5.0 (X11; Linux x86_64; rv:109.0) Gecko/20100101 Firefox/119.0',
'timeout'=>5
]
];
if (isset($conf['proxy']) && (is_null($conf['use_proxy_with']) || in_array(strtolower($_GET['host']),$conf['use_proxy_with'])))
$context['http']['proxy']=$conf['proxy'];
$context=stream_context_create($context);
$hhost=htmlentities($_GET['host']);
$acchost=preg_replace('#^@?[^@]+@(.*)$#','$1',$_GET['acctock']);
if ($acchost==$_GET['acctock'] || $acchost=='')
$acchost=$_GET['host'];
$acchostck=false;
$http_response_header=null;
$url=$_GET['host'].'/api/v1/instance/domain_blocks';
$hurl=htmlentities($url);
$res=@file_get_contents('https://'.$url,false,$context);
//echo preprint($http_response_header);
if (isset($http_response_header))
$rl=ckratelimit($http_response_header);
if ($res!==false && is_array($http_response_header) && gethttpcode($http_response_header)=='200' && null!==$res=@json_decode($res,true)) {
foreach ($res as $val) {
if (isset($val['domain'],$val['severity']) && $val['domain']==$acchost) {
if ($val['severity']=='silence')
$acchostck="The whole domain of the account to check, «{$acchost}», is limited by «{$_GET['host']}».";
elseif ($val['severity']=='suspend')
$acchostck="The whole domain of the account to check, «{$acchost}», is suspended by «{$_GET['host']}».";
else
$acchostck="The whole domain of the account to check, «{$acchost}», is moderated by «{$_GET['host']}», but {$usname} could not detect the moderation severity.";
break;
}
}
if ($acchostck===false)
$acchostck="The domain of the account to check, «{$acchost}», is neither limited nor suspended by «{$_GET['host']}».";
} else {
$acchostck="{$usname} could not detect if the domain of the account to check, «{$acchost}», is limited or suspended by «{$_GET['host']}».";
}
$acchostck=htmlentities($acchostck);
$http_response_header=null;
$httpcode=null;
$url=$_GET['host'].'/api/v1/accounts/lookup?acct='.urlencode($_GET['acctock']);
$hacctock=htmlentities($_GET['acctock']);
$hurl=htmlentities($url);
$res=@file_get_contents('https://'.$url,false,$context);
//echo preprint($http_response_header);
if (isset($http_response_header)) {
$httpcode=gethttpcode($http_response_header);
$rl=ckratelimit($http_response_header);
}
if ($res===false) {
echo "<div class=\"error\">Error: could not connect to «{$hhost}».</div>\n";
} elseif (isset($httpcode) && $httpcode!='200') {
echo "<div class=\"error\">Error: «{$hurl}» returned an HTTP code that is different than «200»: <a href=\"https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/{$httpcode}\">{$httpcode}</a>.</div>\n";
} elseif (null===$res=@json_decode($res,true)) {
echo "<div class=\"error\">Error: «{$hurl}» returned data that could not be parsed as JSON (".json_last_error().': '.json_last_error_msg().").</div>\n";
} elseif (isset($res['error'])) {
if ($res['error']=='Record not found')
echo "<div class=\"neutral\"{$hhost}» does not know the «{$hacctock}» account.<br><br>{$acchostck}</div>\n";
else
echo "<div class=\"error\">Error: «{$hurl}» replied with this error message: «".htmlentities($res['error'])."».</div>\n";
} elseif (!isset($res['id'])) {
echo "<div class=\"error\">Error: «{$hurl}» returned data in an unexpected format.</div>\n";
} else {
$out="<div class=\"neutral\">\n«{$hacctock}» ";
(isset($res['limited']) && $res['limited']) ? $out.='is' : $out.='is not';
$out.=" limited by «{$hhost}».<br>\n«{$hacctock}» ";
(isset($res['suspended']) && $res['suspended']) ? $out.='is' : $out.='is not';
$out.=" suspended by «{$hhost}».<br>\n<br>\n{$acchostck}</div>\n";
echo $out;
}
if (is_array($rl) && isset($rl['ok']) && $rl['ok'] && @file_put_contents($rlfp,$rl['remaining']."\n".($rl['sleep']+time())."\n")===false)
echo "<div class=\"warning\">Warning: could not write to rate limit state file.</div>\n";
}
if (isset($conf['footer']))
echo "<div id=\"almfooter\">{$conf['footer']}</div>\n";
echo "<div id=\"footer\"><a href=\"".SREPO."\">{$usname} ".SVERS."</a></div>
</div>
</body>
</html>\n";
function preprint($var) {
return '<pre>'.print_r($var,true)."</pre>\n";
}
function gethttpcode($headers) {
return preg_replace('#^[^ ]+ (\d+).*$#','$1',$headers[0]);
}
?>