index.php 9.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201
  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. // example of a post censored by mastodon.uno: https://livellosegreto.it/@xabacadabra/110662830871887169
  15. require 'lib/ght.php';
  16. require 'lib/ckratelimit.php';
  17. const SNAME='unocck';
  18. const SVERS='0.1.1';
  19. const SREPO='https://git.lattuga.net/jones/unocck';
  20. const MULEN=1024;
  21. const INIFP='sec/conf.ini';
  22. const RLFP='sec/rl.state';
  23. header('Content-Language: en');
  24. $usname=ucfirst(SNAME);
  25. $timeout=5;
  26. $cjr=rand(0,999999);
  27. $conf=@parse_ini_file(INIFP,false,INI_SCANNER_RAW);
  28. if ($conf===false)
  29. die('Could not open configuration file.');
  30. elseif (!isset($conf['host']) || !isset($conf['hostdesc']) || !isset($conf['token']) || !isset($conf['maintref']))
  31. die('Configuration file is malformed.');
  32. if (file_exists(RLFP)) {
  33. $buf=@file(RLFP,FILE_IGNORE_NEW_LINES);
  34. if ($buf===false)
  35. die('Could not open rate limiting state file.');
  36. if (count($buf)!=2 || preg_match('#^\d+$#',$buf[0])!==1 || preg_match('#^\d+$#',$buf[1])!==1)
  37. die('Malformed rate limiting state file.');
  38. $rl=['remaining'=>$buf[0]+0,'restime'=>$buf[1]+0];
  39. } else {
  40. $rl=['remaining'=>400,'restime'=>0];
  41. }
  42. $now=time();
  43. if ($rl['remaining']==10 && $now<=$rl['restime'])// ten to leave a margin for "many people using it"
  44. die("Sorry, this {$usname} instance has reached rate limit on «{$conf['host']}», please wait at least ".ght($rl['restime']-$now,null,0).'.');
  45. $errors=[];
  46. if (isset($argv[1]))
  47. $_GET=['purl'=>$argv[1]];
  48. if (isset($_GET['purl'])) {
  49. if (strlen($_GET['purl'])>MULEN) {
  50. $_GET['purl']='';
  51. $errors[]='“Post URL” is too long';
  52. }
  53. $_GET['purl']=trim($_GET['purl']);
  54. if ($_GET['purl']!='' && preg_match('#^https?://#',$_GET['purl'])!==1)// todo: make it better
  55. $errors[]='“Post URL” is not a valid http(s) address';
  56. } else {
  57. $_GET['purl']='';
  58. }
  59. if ($_GET['purl']!=='')
  60. $purlhn=preg_replace('#^https?://([^/]+).*$#','$1',$_GET['purl']);
  61. if (count($errors)>0)
  62. $errors='<div class="warning">There are some errors in the values you submitted<ul><li>'.implode('</li><li>',$errors).'</li></ul></div><div class="horsep"></div>';
  63. else
  64. $errors='';
  65. echo "<!DOCTYPE HTML>
  66. <html lang=\"en\">
  67. <head>
  68. <title>{$usname}</title>
  69. <meta http-equiv=\"Content-Type\" content=\"text/html; charset=utf-8\">
  70. <meta name=\"description\" content=\"A tool to check if {$conf['host']} is censoring a given post\">
  71. <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no\">
  72. <meta property=\"og:image\" content=\"imgs/ogimage.png\">
  73. <link rel=\"icon\" type=\"image/png\" href=\"imgs/icon-16.png\" sizes=\"16x16\">
  74. <link rel=\"icon\" type=\"image/png\" href=\"imgs/icon-32.png\" sizes=\"32x32\">
  75. <link rel=\"icon\" type=\"image/png\" href=\"imgs/icon-192.png\" sizes=\"192x192\">
  76. <link rel=\"icon\" type=\"image/png\" href=\"imgs/icon-512.png\" sizes=\"512x512\">
  77. <link rel=\"apple-touch-icon-precomposed\" href=\"imgs/icon-180.png\">
  78. <link rel=\"stylesheet\" type=\"text/css\" href=\"css/main.css?v={$cjr}\">
  79. </head>
  80. <body>
  81. <div id=\"main\">
  82. <h1>{$usname}</h1>
  83. <div class=\"normtext\">
  84. <p class=\"firstp\">Hello, this is a {$usname} instance. {$usname} is a tool to easily check if a Mastodon instance is censoring a given fediverse post. This {$usname} instance is set to check «{$conf['host']}», {$conf['hostdesc']}. It works by querying {$conf['host']}’s <a href=\"https://docs.joinmastodon.org/methods/search/#v2\">/api/v2/search</a> API endpoint with the credentials of an application defined inside a {$conf['host']} account, but to avoid false positives it does so only after it has verified that the URL you passed to it actually points to a publicly accessible ActivityPub object.</p>
  85. <p>{$usname} does not use cookies or Javascript and does not store any data about you anywhere.</p>
  86. <p>You can find {$usname}’s code <a href=\"".SREPO."\">here</a>.</p>
  87. <p>This {$usname} instance is maintained by {$conf['maintref']}.</p>
  88. </div>
  89. <div class=\"horsep\"></div>
  90. ".$errors."
  91. <h2>Check</h2>
  92. <form method=\"get\" id=\"mainform\" name=\"mainform\">
  93. <div class=\"inputdiv\"><label for=\"purl\">Post URL</label><input type=\"text\" id=\"purl\" name=\"purl\" class=\"input\" placeholder=\"Example: https://mastodon.whatever.net/@user/110123412349306591\" value=\"{$_GET['purl']}\" maxlength=\"".MULEN."\" required></div>
  94. <div class=\"lastinputdiv\"><button type=\"submit\" id=\"button\" class=\"button\">Check</button></div>\n</form>\n";
  95. if ($errors=='' && $_GET['purl']!='') {
  96. echo "<div class=\"horsep\"></div>\n";
  97. $context=[
  98. 'http'=>[
  99. 'header'=>"Content-type: application/x-www-form-urlencoded\r\nAccept: application/ld+json; profile=\"https://www.w3.org/ns/activitystreams\"\r\n",
  100. 'method'=>'GET',
  101. 'ignore_errors'=>true,
  102. 'user_agent'=>'Mozilla/5.0 (X11; Linux x86_64; rv:109.0) Gecko/20100101 Firefox/115.0',
  103. 'timeout'=>5
  104. ]
  105. ];
  106. if (isset($conf['proxy']))
  107. $context['http']['proxy']=$conf['proxy'];
  108. $http_response_header=null;
  109. $res=@file_get_contents($_GET['purl'],false,stream_context_create($context));
  110. /*$xres=json_decode($res,true);
  111. echo preprint($xres);*/
  112. if (isset($http_response_header))
  113. $hcode=gethttpcode($http_response_header);
  114. if ($res===false || !isset($http_response_header)) {
  115. echo "<div class=\"error\">Error: {$usname} could not connect to «{$purlhn}» and won’t proceed with the check.</div>\n";
  116. } elseif ($hcode[0]!='2') {
  117. if ($hcode[0]=='5')
  118. echo "<div class=\"error\">«{$purlhn}» returned a server error. {$usname} won’t proceed with the check.</div>\n";
  119. elseif ($hcode[0]=='4')
  120. echo "<div class=\"error\">Error: {$usname} could not access the “Post URL” you passed to it: probably the post visibility is not public/unlisted, or you passed a wrong URL. {$usname} won’t proceed with the check.</div>\n";
  121. elseif ($hcode[0]=='3')
  122. echo "<div class=\"error\">Error: the “Post URL” you passed to {$usname} redirects, and since its programmer is lazy, {$usname} currently only accepts URLs which point to original posts (on Mastodon web you can copy a post’s original URL by opening the “three vertical dots” menu you find on every post and selecting “Copy link to post”). {$usname} won’t proceed with the check.</div>\n";
  123. elseif ($hcode[0]=='1')
  124. echo "<div class=\"error\">«{$purlhn}» returned an unexpected and useless informational message. {$usname} won’t proceed with the check.</div>\n";
  125. else
  126. echo "<div class=\"error\">«{$purlhn}» returned an unexpected HTTP code. {$usname} won’t proceed with the check.</div>\n";
  127. } elseif (null===$res=@json_decode($res,true)) {
  128. echo "<div class=\"error\">Error: «{$purlhn}» returned data which could not be parsed as JSON (".json_last_error().': '.json_last_error_msg().").</div>\n";
  129. } elseif (isset($res['error'])) {
  130. echo "<div class=\"error\">Error: «{$purlhn}» returned «".htmlentities($res['error'])."». {$usname} won’t proceed with the check.</div>\n";
  131. } elseif (!isset($res['@context'][0]) || $res['@context'][0]!='https://www.w3.org/ns/activitystreams') {
  132. echo "<div class=\"error\">Error: the “Post URL” you passed to {$usname} doesn’t point to an ActivityPub post. {$usname} won’t proceed with the check.</div>\n";
  133. } else {
  134. $context['http']['header']="Content-type: application/x-www-form-urlencoded\r\nAccept: application/json\r\nAuthorization: Bearer {$conf['token']}\r\n";
  135. $http_response_header=null;
  136. $hhost=htmlentities($conf['host']);
  137. $url=$conf['host'].'/api/v2/search?q='.urlencode($_GET['purl']).'&type=statuses&resolve=1&limit=1';
  138. $hurl=htmlentities($url);
  139. //while (true) {
  140. $res=@file_get_contents('https://'.$url,false,stream_context_create($context));
  141. if (isset($http_response_header))
  142. $rl=ckratelimit($http_response_header);
  143. //echo preprint($rl);
  144. if (isset($rl['ok']) && $rl['ok'] && @file_put_contents(RLFP,$rl['remaining']."\n".($rl['sleep']+time())."\n")===false)
  145. echo "<div class=\"warning\">Warning: could not write to rate limit state file.</div>\n";
  146. /*if ($rl['remaining']==0)
  147. break;
  148. usleep(250000);
  149. }*/
  150. if ($res===false) {
  151. echo "<div class=\"error\">Error: could not connect to «{$hhost}».</div>\n";
  152. } elseif (null===$res=@json_decode($res,true)) {
  153. echo "<div class=\"error\">Error: «{$hurl}» returned data which could not be parsed as JSON (".json_last_error().': '.json_last_error_msg().").</div>\n";
  154. } elseif (isset($res['error'])) {
  155. echo "<div class=\"error\">Error: «{$hurl}» replied with this error message: «".htmlentities($res['error'])."».</div>\n";
  156. } elseif (!isset($res['statuses'])) {
  157. echo "<div class=\"error\">Error: «{$hurl}» returned data in an unexpected format.</div>\n";
  158. } else {
  159. if (isset($res['statuses'][0]['id']))
  160. echo "<div class=\"neutral\">\nPost is not censored.\n</div>\n";
  161. else
  162. echo "<div class=\"neutral\">\nPost is censored.\n</div>\n";
  163. }
  164. }
  165. }
  166. if (isset($conf['footer']))
  167. echo "<div id=\"almfooter\">{$conf['footer']}</div>\n";
  168. echo "<div id=\"footer\"><a href=\"".SREPO."\">".SNAME." ".SVERS."</a></div>
  169. </div>
  170. </body>
  171. </html>\n";
  172. function preprint($var) {
  173. return '<pre>'.print_r($var,true)."</pre>\n";
  174. }
  175. function gethttpcode($headers) {
  176. return preg_replace('#^[^ ]+ (\d+).*$#','$1',$headers[0]);
  177. }
  178. ?>