acck/index.php

240 lines
12 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.5';
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);
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 “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 “Account address” is not a valid fediverse account';
} else {
$_GET['acctock']='';
}
$hostok=false;
if (isset($_GET['host'])) {
if (strlen($_GET['host'])>MAXHOSTLEN) {
$_GET['host']='';
$errors[]='Value for “Server domain” is too long';
}
$_GET['host']=trim($_GET['host']);
if ($_GET['host']!='' && !validhostname($_GET['host']))
$errors[]='Value for “Server domain” is not a valid hostname';
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 server\">
<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 moderated (<a href=\"https://docs.joinmastodon.org/admin/moderation/#limit-user\">silenced, aka “limited”</a>, and-or <a href=\"https://docs.joinmastodon.org/admin/moderation/#suspend-user\">suspended, aka “blocked”</a>) by a Mastodon server (aka “instance”).</p>
<p>Since an account can be reported as moderated by a Mastodon server even when the server is moderating the whole accounts domain, {$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>, which 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>, which 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 me on Mastodon <a href=\"https://puntarella.party/@j0nes\">here</a>.</p>
</div>
<div class=\"horsep\"></div>
".$errors."
<h2>Check</h2>
<form method=\"get\" id=\"mainform\" name=\"mainform\">
<div class=\"inputdiv\"><label for=\"acctock\">Account address</label><input type=\"text\" id=\"acctock\" name=\"acctock\" class=\"input\" placeholder=\"Example: paperino@paperopoli.net\" value=\"{$_GET['acctock']}\" maxlength=\"".MAXACCLEN."\" required></div>
<div class=\"inputdiv\"><label for=\"host\">Mastodon server (“instance”) domain</label><input type=\"text\" id=\"host\" name=\"host\" class=\"input\" placeholder=\"Example: topolinia.net\" 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']))
$context['http']['proxy']=$conf['proxy'];
$http_response_header=null;
$context=stream_context_create($context);
$hhost=htmlentities($_GET['host']);
$url=$_GET['host'].'/api/v1/instance';
$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,'echofun',true,false);
if ($res===false) {
echo "<div class=\"error\">Error: could not connect to «{$hhost}».</div>\n";
} elseif (is_array($http_response_header) && gethttpcode($http_response_header)!='200') {
echo "<div class=\"error\">Error: «{$hhost}» seems not to be running Mastodon.</div>\n";
} elseif (null===$res=@json_decode($res,true)) {
echo "<div class=\"error\">Error: «{$hurl}» returned data which could not be parsed as JSON (".json_last_error().': '.json_last_error_msg().").</div>\n";
} elseif (isset($res['error'])) {
echo "<div class=\"error\">Error: «{$hurl}» replied with this error message: «".htmlentities($res['error'])."».</div>\n";
} elseif (!isset($res['version'])) {
echo "<div class=\"error\">Error: «{$hurl}» returned data in an unexpected format.</div>\n";
} elseif (preg_replace('#[^\d\.].*#','',$res['version'])<'3.4.0') {
echo "<div class=\"error\">Error: «{$hhost}» is running a version of Mastodon that is earlier than 3.4.0 («{$res['version']}»).</div>\n";
} else {
$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,'echofun',true,false);
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 silenced 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 not moderated (silenced or suspended) by «{$_GET['host']}».";
} else {
$acchostck="{$usname} could not detect if the domain of the account to check, «{$acchost}», is moderated (silenced or suspended) by «{$_GET['host']}».";
}
$acchostck=htmlentities($acchostck);
$http_response_header=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))
$rl=ckratelimit($http_response_header,'echofun',true,false);
if ($res===false) {
echo "<div class=\"error\">Error: could not connect to «{$hhost}».</div>\n";
} elseif (null===$res=@json_decode($res,true)) {
echo "<div class=\"error\">Error: «{$hurl}» returned data which 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\">Server «{$hhost}» doesnt know «{$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.=" silenced by «{$hhost}»<br>\n«{$hacctock}» ";
(isset($res['suspended']) && $res['suspended']) ? $out.='is' : $out.='is not';
$out.=" suspended by «{$hhost}»<br><br>\n{$acchostck}</div>\n";
echo $out;
}
}
if (is_array($rl) && @file_put_contents($rlfp,$rl['remaining']."\n".($rl['secstoreset']+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."\">".SNAME." ".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]);
}
function echofun($msg) {
echo $msg;
}
?>