unocck/index.php

201 lines
9.6 KiB
PHP
Raw Permalink 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/>.
*/
// example of a post censored by mastodon.uno: https://livellosegreto.it/@xabacadabra/110662830871887169
require 'lib/ght.php';
require 'lib/ckratelimit.php';
const SNAME='unocck';
const SVERS='0.1.1';
const SREPO='https://git.lattuga.net/jones/unocck';
const MULEN=1024;
const INIFP='sec/conf.ini';
const RLFP='sec/rl.state';
header('Content-Language: en');
$usname=ucfirst(SNAME);
$timeout=5;
$cjr=rand(0,999999);
$conf=@parse_ini_file(INIFP,false,INI_SCANNER_RAW);
if ($conf===false)
die('Could not open configuration file.');
elseif (!isset($conf['host']) || !isset($conf['hostdesc']) || !isset($conf['token']) || !isset($conf['maintref']))
die('Configuration file is malformed.');
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];
} else {
$rl=['remaining'=>400,'restime'=>0];
}
$now=time();
if ($rl['remaining']==10 && $now<=$rl['restime'])// ten to leave a margin for "many people using it"
die("Sorry, this {$usname} instance has reached rate limit on «{$conf['host']}», please wait at least ".ght($rl['restime']-$now,null,0).'.');
$errors=[];
if (isset($argv[1]))
$_GET=['purl'=>$argv[1]];
if (isset($_GET['purl'])) {
if (strlen($_GET['purl'])>MULEN) {
$_GET['purl']='';
$errors[]='“Post URL” is too long';
}
$_GET['purl']=trim($_GET['purl']);
if ($_GET['purl']!='' && preg_match('#^https?://#',$_GET['purl'])!==1)// todo: make it better
$errors[]='“Post URL” is not a valid http(s) address';
} else {
$_GET['purl']='';
}
if ($_GET['purl']!=='')
$purlhn=preg_replace('#^https?://([^/]+).*$#','$1',$_GET['purl']);
if (count($errors)>0)
$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>';
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 {$conf['host']} is censoring a given post\">
<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 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>
<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>.</p>
<p>This {$usname} instance is maintained by {$conf['maintref']}.</p>
</div>
<div class=\"horsep\"></div>
".$errors."
<h2>Check</h2>
<form method=\"get\" id=\"mainform\" name=\"mainform\">
<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>
<div class=\"lastinputdiv\"><button type=\"submit\" id=\"button\" class=\"button\">Check</button></div>\n</form>\n";
if ($errors=='' && $_GET['purl']!='') {
echo "<div class=\"horsep\"></div>\n";
$context=[
'http'=>[
'header'=>"Content-type: application/x-www-form-urlencoded\r\nAccept: application/ld+json; profile=\"https://www.w3.org/ns/activitystreams\"\r\n",
'method'=>'GET',
'ignore_errors'=>true,
'user_agent'=>'Mozilla/5.0 (X11; Linux x86_64; rv:109.0) Gecko/20100101 Firefox/115.0',
'timeout'=>5
]
];
if (isset($conf['proxy']))
$context['http']['proxy']=$conf['proxy'];
$http_response_header=null;
$res=@file_get_contents($_GET['purl'],false,stream_context_create($context));
/*$xres=json_decode($res,true);
echo preprint($xres);*/
if (isset($http_response_header))
$hcode=gethttpcode($http_response_header);
if ($res===false || !isset($http_response_header)) {
echo "<div class=\"error\">Error: {$usname} could not connect to «{$purlhn}» and wont proceed with the check.</div>\n";
} elseif ($hcode[0]!='2') {
if ($hcode[0]=='5')
echo "<div class=\"error\"{$purlhn}» returned a server error. {$usname} wont proceed with the check.</div>\n";
elseif ($hcode[0]=='4')
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} wont proceed with the check.</div>\n";
elseif ($hcode[0]=='3')
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 posts original URL by opening the “three vertical dots” menu you find on every post and selecting “Copy link to post”). {$usname} wont proceed with the check.</div>\n";
elseif ($hcode[0]=='1')
echo "<div class=\"error\"{$purlhn}» returned an unexpected and useless informational message. {$usname} wont proceed with the check.</div>\n";
else
echo "<div class=\"error\"{$purlhn}» returned an unexpected HTTP code. {$usname} wont proceed with the check.</div>\n";
} elseif (null===$res=@json_decode($res,true)) {
echo "<div class=\"error\">Error: «{$purlhn}» 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: «{$purlhn}» returned «".htmlentities($res['error'])."». {$usname} wont proceed with the check.</div>\n";
} elseif (!isset($res['@context'][0]) || $res['@context'][0]!='https://www.w3.org/ns/activitystreams') {
echo "<div class=\"error\">Error: the “Post URL” you passed to {$usname} doesnt point to an ActivityPub post. {$usname} wont proceed with the check.</div>\n";
} else {
$context['http']['header']="Content-type: application/x-www-form-urlencoded\r\nAccept: application/json\r\nAuthorization: Bearer {$conf['token']}\r\n";
$http_response_header=null;
$hhost=htmlentities($conf['host']);
$url=$conf['host'].'/api/v2/search?q='.urlencode($_GET['purl']).'&type=statuses&resolve=1&limit=1';
$hurl=htmlentities($url);
//while (true) {
$res=@file_get_contents('https://'.$url,false,stream_context_create($context));
if (isset($http_response_header))
$rl=ckratelimit($http_response_header);
//echo preprint($rl);
if (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 ($rl['remaining']==0)
break;
usleep(250000);
}*/
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'])) {
echo "<div class=\"error\">Error: «{$hurl}» replied with this error message: «".htmlentities($res['error'])."».</div>\n";
} elseif (!isset($res['statuses'])) {
echo "<div class=\"error\">Error: «{$hurl}» returned data in an unexpected format.</div>\n";
} else {
if (isset($res['statuses'][0]['id']))
echo "<div class=\"neutral\">\nPost is not censored.\n</div>\n";
else
echo "<div class=\"neutral\">\nPost is censored.\n</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]);
}
?>