219 lines
11 KiB
PHP
219 lines
11 KiB
PHP
<?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 account’s 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 server’s <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 account’s domain, {$usname} tries to use the server’s <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 server’s admins, and was introduced in Mastodon v4.0.0. If it can’t 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]);
|
||
}
|
||
|
||
?>
|