index.php 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240
  1. <?php
  2. /*
  3. This program is free software: you can redistribute it and/or modify
  4. it under the terms of the GNU General Public License as published by
  5. the Free Software Foundation, either version 3 of the License, or
  6. (at your option) any later version.
  7. This program is distributed in the hope that it will be useful,
  8. but WITHOUT ANY WARRANTY; without even the implied warranty of
  9. MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  10. GNU General Public License for more details.
  11. You should have received a copy of the GNU General Public License
  12. along with this program. If not, see <http://www.gnu.org/licenses/>.
  13. */
  14. require 'lib/validhostname.php';
  15. require 'lib/ckratelimit.php';
  16. require 'lib/ght.php';
  17. const SNAME='acck';
  18. const SVERS='0.1.5';
  19. const SREPO='https://git.lattuga.net/jones/acck';
  20. const MAXACCLEN=30+253+2;
  21. const MAXHOSTLEN=253;
  22. const INIFP='sec/conf.ini';
  23. $usname=ucfirst(SNAME);
  24. $timeout=5;
  25. $cjr=rand(0,999999);
  26. header('Content-Language: en');
  27. if (file_exists(INIFP)) {
  28. $conf=@parse_ini_file(INIFP,false,INI_SCANNER_RAW);
  29. if ($conf===false)
  30. die('Configuration file «'.INIFP."» exists but {$usname} could not open it.");
  31. }
  32. $errors=[];
  33. if (isset($_GET['acctock'])) {
  34. if (strlen($_GET['acctock'])>MAXACCLEN) {
  35. $_GET['acctock']='';
  36. $errors[]='Value for “Account address” is too long';
  37. }
  38. $_GET['acctock']=trim($_GET['acctock']);
  39. if ($_GET['acctock']!='' && preg_match('#^@?[0-9a-zA-Z_]+(@[a-z0-9.-]{4,253})?$#',$_GET['acctock'])!==1)// todo: make it better, like split ecc.
  40. $errors[]='Value for “Account address” is not a valid Mastodon account';
  41. } else {
  42. $_GET['acctock']='';
  43. }
  44. $hostok=false;
  45. if (isset($_GET['host'])) {
  46. if (strlen($_GET['host'])>MAXHOSTLEN) {
  47. $_GET['host']='';
  48. $errors[]='Value for “Server domain” is too long';
  49. }
  50. $_GET['host']=trim($_GET['host']);
  51. if ($_GET['host']!='' && !validhostname($_GET['host']))
  52. $errors[]='Value for “Server domain” is not a valid hostname';
  53. else
  54. $hostok=true;
  55. } else {
  56. $_GET['host']='';
  57. }
  58. $rl=['remaining'=>400,'restime'=>0];
  59. if ($hostok) {
  60. $rlfp='sec/'.$_GET['host'].'.rl.state';
  61. if (file_exists($rlfp)) {
  62. $buf=@file($rlfp,FILE_IGNORE_NEW_LINES);
  63. if ($buf===false)
  64. die('Could not open rate limiting state file.');
  65. if (count($buf)!=2 || preg_match('#^\d+$#',$buf[0])!==1 || preg_match('#^\d+$#',$buf[1])!==1)
  66. die('Malformed rate limiting state file.');
  67. $rl=['remaining'=>$buf[0]+0,'restime'=>$buf[1]+0];
  68. }
  69. }
  70. $now=time();
  71. 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
  72. $errors[]="This {$usname} instance has reached rate limit on «{$_GET['host']}», please wait at least ".ght($rl['restime']-$now,null,0).'.';
  73. if (count($errors)>0)
  74. $errors='<div class="warning">There are some errors<ul><li>'.implode('</li><li>',$errors).'</li></ul></div><div class="horsep"></div>';
  75. else
  76. $errors='';
  77. echo "<!DOCTYPE HTML>
  78. <html lang=\"en\">
  79. <head>
  80. <title>{$usname}</title>
  81. <meta http-equiv=\"Content-Type\" content=\"text/html; charset=utf-8\">
  82. <meta name=\"description\" content=\"A tool to check if a Mastodon account is moderated by a Mastodon server\">
  83. <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no\">
  84. <meta property=\"og:image\" content=\"imgs/ogimage.png\">
  85. <link rel=\"icon\" type=\"image/png\" href=\"imgs/icon-16.png\" sizes=\"16x16\">
  86. <link rel=\"icon\" type=\"image/png\" href=\"imgs/icon-32.png\" sizes=\"32x32\">
  87. <link rel=\"icon\" type=\"image/png\" href=\"imgs/icon-192.png\" sizes=\"192x192\">
  88. <link rel=\"icon\" type=\"image/png\" href=\"imgs/icon-512.png\" sizes=\"512x512\">
  89. <link rel=\"apple-touch-icon-precomposed\" href=\"imgs/icon-180.png\">
  90. <link rel=\"stylesheet\" type=\"text/css\" href=\"css/main.css?v={$cjr}\">
  91. </head>
  92. <body>
  93. <div id=\"main\">
  94. <h1>{$usname}</h1>
  95. <div class=\"normtext\">
  96. <p class=\"firstp\">Hello, this is {$usname}, a tool to easily check if a Mastodon 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>
  97. <p>Since an account can be reported as moderated by a Mastodon server even when the server is moderating the whole account’s domain, {$usname} also tries to detect if this is the case, and tells about it.</p>
  98. <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>, which 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>, which 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>
  99. <p>{$usname} does not use cookies or Javascript and does not store any data about you anywhere.</p>
  100. <p>You can find {$usname}’s code <a href=\"".SREPO."\">here</a>, and you can contact me on Mastodon <a href=\"https://puntarella.party/@umpi\">here</a>.</p>
  101. </div>
  102. <div class=\"horsep\"></div>
  103. ".$errors."
  104. <h2>Check</h2>
  105. <form method=\"get\" id=\"mainform\" name=\"mainform\">
  106. <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>
  107. <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>
  108. <div class=\"lastinputdiv\"><button type=\"submit\" id=\"button\" class=\"button\">Check</button></div>\n</form>\n";
  109. if ($errors=='' && $_GET['acctock']!='' && $_GET['host']!='') {
  110. echo "<div class=\"horsep\"></div>\n";
  111. $context=[
  112. 'http'=>[
  113. 'method'=>'GET',
  114. 'ignore_errors'=>true,
  115. 'protocol_version'=>1.1,
  116. 'user_agent'=>'Mozilla/5.0 (X11; Linux x86_64; rv:109.0) Gecko/20100101 Firefox/119.0',
  117. 'timeout'=>5
  118. ]
  119. ];
  120. if (isset($conf['proxy']))
  121. $context['http']['proxy']=$conf['proxy'];
  122. $http_response_header=null;
  123. $context=stream_context_create($context);
  124. $hhost=htmlentities($_GET['host']);
  125. $url=$_GET['host'].'/api/v1/instance';
  126. $hurl=htmlentities($url);
  127. $res=@file_get_contents('https://'.$url,false,$context);
  128. //echo preprint($http_response_header);
  129. if (isset($http_response_header))
  130. $rl=ckratelimit($http_response_header,'echofun',true,false);
  131. if ($res===false) {
  132. echo "<div class=\"error\">Error: could not connect to «{$hhost}».</div>\n";
  133. } elseif (is_array($http_response_header) && gethttpcode($http_response_header)!='200') {
  134. echo "<div class=\"error\">Error: «{$hhost}» seems not to be running Mastodon.</div>\n";
  135. } elseif (null===$res=@json_decode($res,true)) {
  136. echo "<div class=\"error\">Error: «{$hurl}» returned data which could not be parsed as JSON (".json_last_error().': '.json_last_error_msg().").</div>\n";
  137. } elseif (isset($res['error'])) {
  138. echo "<div class=\"error\">Error: «{$hurl}» replied with this error message: «".htmlentities($res['error'])."».</div>\n";
  139. } elseif (!isset($res['version'])) {
  140. echo "<div class=\"error\">Error: «{$hurl}» returned data in an unexpected format.</div>\n";
  141. } elseif (preg_replace('#[^\d\.].*#','',$res['version'])<'3.4.0') {
  142. echo "<div class=\"error\">Error: «{$hhost}» is running a version of Mastodon that is earlier than 3.4.0 («{$res['version']}»).</div>\n";
  143. } else {
  144. $acchost=preg_replace('#^@?[^@]+@(.*)$#','$1',$_GET['acctock']);
  145. if ($acchost==$_GET['acctock'] || $acchost=='')
  146. $acchost=$_GET['host'];
  147. $acchostck=false;
  148. $http_response_header=null;
  149. $url=$_GET['host'].'/api/v1/instance/domain_blocks';
  150. $hurl=htmlentities($url);
  151. $res=@file_get_contents('https://'.$url,false,$context);
  152. //echo preprint($http_response_header);
  153. if (isset($http_response_header))
  154. $rl=ckratelimit($http_response_header,'echofun',true,false);
  155. if ($res!==false && is_array($http_response_header) && gethttpcode($http_response_header)=='200' && null!==$res=@json_decode($res,true)) {
  156. foreach ($res as $val) {
  157. if (isset($val['domain'],$val['severity']) && $val['domain']==$acchost) {
  158. if ($val['severity']=='silence')
  159. $acchostck="The whole domain of the account to check, «{$acchost}», is silenced by «{$_GET['host']}».";
  160. elseif ($val['severity']=='suspend')
  161. $acchostck="The whole domain of the account to check, «{$acchost}», is suspended by «{$_GET['host']}».";
  162. else
  163. $acchostck="The whole domain of the account to check, «{$acchost}», is moderated by «{$_GET['host']}», but {$usname} could not detect the moderation severity.";
  164. break;
  165. }
  166. }
  167. if ($acchostck===false)
  168. $acchostck="The domain of the account to check, «{$acchost}», is not moderated (silenced or suspended) by «{$_GET['host']}».";
  169. } else {
  170. $acchostck="{$usname} could not detect if the domain of the account to check, «{$acchost}», is moderated (silenced or suspended) by «{$_GET['host']}».";
  171. }
  172. $acchostck=htmlentities($acchostck);
  173. $http_response_header=null;
  174. $url=$_GET['host'].'/api/v1/accounts/lookup?acct='.urlencode($_GET['acctock']);
  175. $hacctock=htmlentities($_GET['acctock']);
  176. $hurl=htmlentities($url);
  177. $res=@file_get_contents('https://'.$url,false,$context);
  178. //echo preprint($http_response_header);
  179. if (isset($http_response_header))
  180. $rl=ckratelimit($http_response_header,'echofun',true,false);
  181. if ($res===false) {
  182. echo "<div class=\"error\">Error: could not connect to «{$hhost}».</div>\n";
  183. } elseif (null===$res=@json_decode($res,true)) {
  184. echo "<div class=\"error\">Error: «{$hurl}» returned data which could not be parsed as JSON (".json_last_error().': '.json_last_error_msg().").</div>\n";
  185. } elseif (isset($res['error'])) {
  186. if ($res['error']=='Record not found')
  187. echo "<div class=\"neutral\">Server «{$hhost}» doesn’t know «{$hacctock}» account.<br><br>{$acchostck}</div>\n";
  188. else
  189. echo "<div class=\"error\">Error: «{$hurl}» replied with this error message: «".htmlentities($res['error'])."».</div>\n";
  190. } elseif (!isset($res['id'])) {
  191. echo "<div class=\"error\">Error: «{$hurl}» returned data in an unexpected format.</div>\n";
  192. } else {
  193. $out="<div class=\"neutral\">\n«{$hacctock}» ";
  194. (isset($res['limited']) && $res['limited']) ? $out.='is' : $out.='is not';
  195. $out.=" silenced by «{$hhost}»<br>\n«{$hacctock}» ";
  196. (isset($res['suspended']) && $res['suspended']) ? $out.='is' : $out.='is not';
  197. $out.=" suspended by «{$hhost}»<br><br>\n{$acchostck}</div>\n";
  198. echo $out;
  199. }
  200. }
  201. if (is_array($rl) && @file_put_contents($rlfp,$rl['remaining']."\n".($rl['secstoreset']+time())."\n")===false)
  202. echo "<div class=\"warning\">Warning: could not write to rate limit state file.</div>\n";
  203. }
  204. if (isset($conf['footer']))
  205. echo "<div id=\"almfooter\">{$conf['footer']}</div>\n";
  206. echo "<div id=\"footer\"><a href=\"".SREPO."\">".SNAME." ".SVERS."</a></div>
  207. </div>
  208. </body>
  209. </html>\n";
  210. function preprint($var) {
  211. return '<pre>'.print_r($var,true)."</pre>\n";
  212. }
  213. function gethttpcode($headers) {
  214. return preg_replace('#^[^ ]+ (\d+).*$#','$1',$headers[0]);
  215. }
  216. function echofun($msg) {
  217. echo $msg;
  218. }
  219. ?>