Browse Source

First commit

pezcurrel 6 tháng trước cách đây
commit
5af448bbd6
12 tập tin đã thay đổi với 555 bổ sung0 xóa
  1. 29 0
      README.md
  2. 233 0
      css/main.css
  3. BIN
      imgs/icon-16.png
  4. BIN
      imgs/icon-180.png
  5. BIN
      imgs/icon-192.png
  6. BIN
      imgs/icon-32.png
  7. BIN
      imgs/icon-512.png
  8. BIN
      imgs/ogimage.png
  9. 205 0
      index.php
  10. 36 0
      lib/ckratelimit.php
  11. 50 0
      lib/ght.php
  12. 2 0
      sec/.htaccess

+ 29 - 0
README.md

@@ -0,0 +1,29 @@
+## What is this?
+
+Unocck is a web tool that allows to easily check if a Mastodon or Mastodon compatible instance is censoring a given ActivityPub post.
+
+## Setting up Unocck on a webserver
+
+To set up Unocck on a webserver you just need it to support PHP, to make the “sec” directory writeable by the webserver’s user and to put into it a “conf.ini” file like this:
+
+```
+host=[hostname of the instance to check]
+hostdesc=[a terse description of the instance, in html]
+maintref=[a terse reference to you, in html]
+token=[the access token of an application that you have already set up on an account on the instance defined by “host” (in Mastodon web UI you can set up an application under “Preferences” -> “Development”; the only privilege it needs is “read:search”)]
+```
+
+You can optionally also set a “footer” that will be added before the link to this repo in page footer, and a proxy to use for connections; for example:
+
+```
+footer=<a href="https://my.server/">Home</a>
+proxy=tcp://localhost:8118
+```
+
+You can change Unocck icons and its “OG Image” by editing files inside the “imgs” directory.
+
+## Are there running Unocck instances?
+
+[Here](https://mastodon.help/unocck/) you can find a running Unocck instance checking mastodon.uno, a Mastodon instance whose admins are well known for [bad practices](https://qua.name/diorama/astroturfing-the-italophone-fediverse) and censoring those who criticize them.
+
+If you set up your own and you want it to be listed here, please let me know using the e-mail address you can find in my profile page.

+ 233 - 0
css/main.css

@@ -0,0 +1,233 @@
+* {
+	box-sizing: border-box;
+}
+
+html {
+	scroll-behavior: smooth;
+}
+
+body {
+	font-family: "sans";
+	background-color: #222222;
+	color: white;
+	margin: 0;
+	padding: 0;
+}
+
+a {
+	/*color: #87decd;*/
+	color: lightgreen;
+}
+
+form {
+	padding: 0;
+	margin: 0;
+}
+
+h1, h2, h3, h4, h5, h6 {
+	text-align: center;
+	color: white;
+}
+
+p {
+	margin: 0;
+	color: white;
+	text-indent: 3mm;
+	/*text-align: justify;
+	-webkit-hyphens: auto;
+	-ms-hyphens: auto;
+	hyphens: auto;*/
+}
+
+.firstp {
+	text-indent: 0;
+}
+
+input:focus, textarea:focus, button:focus {
+	outline: none;
+}
+
+#main {
+	margin-left: auto;
+	margin-right: auto;
+	max-width: 20cm;
+	width: 100%;
+	padding: 3mm;
+}
+
+.error, .success, .warning, .neutral, .normtext {
+	width: 100%;
+	color: red;
+	margin-bottom: 15px;
+	border: 1px solid red;
+	border-radius: 6px;
+	padding: 3mm;
+}
+
+.neutral {
+	color: white;
+	border-color: white;
+}
+
+.warning {
+	color: orange;
+	border-color: orange;
+}
+
+.success {
+	color: lightgreen;
+	border-color: lightgreen;
+}
+
+.normtext {
+	background-color: #444444;
+	color: white;
+	border: none;
+}
+
+.hili {
+	color: yellow;
+}
+
+.tittab {
+	border-collapse: collapse;
+	width: 100%;
+	border: none;
+}
+
+.tittab tr {
+	margin: 0;
+	padding: 0;
+}
+
+.tittab td {
+	margin: 0;
+	padding: 1mm;
+	vertical-align: middle;
+}
+
+.closeb {
+	cursor: pointer;
+	top: 3px;
+	vertical-align: middle;
+}
+
+.inputdiv, .lastinputdiv, .outputdiv, .lastoutputdiv {
+	width: 100%;
+}
+
+.inputdiv {
+	margin-bottom: 15px;	
+}
+
+.lastoutputdiv {
+	margin-top: 15px;
+}
+
+.input, .inputx, .textarea, .button, .halfbutton, .copybutton, .output, .outputnobb, .outputli, .posthead, .lastborder, fieldset {
+	width: 100%;
+	border: 1px solid #555555;
+	border-radius: 0 6px 6px 6px;
+	font-size: 12pt;
+	margin: 0;
+	padding: 3px;
+}
+
+.input, .inputx, .textarea {
+	font-family: "sans";
+}
+
+.inputx {
+	border-radius: 0 6px 0 0;
+}
+
+.lastborder {
+	border-top: none;
+	border-radius: 0 0 6px 6px;
+}
+
+fieldset {
+	padding: 6px;
+	margin-top: 2px;
+}
+
+.halfbutton {
+	width: 50%;
+	height: 30px;
+	border-radius: 6px;
+}
+
+.button, .copybutton {
+	height: 40px;
+	border-radius: 6px;
+	font-weight: bold;
+}
+
+.copybutton {
+	border-top: none;
+	border-radius: 0 0 6px 6px;
+}
+
+.output, .outputnobb {
+	border-radius: 0;
+	margin: 0;
+	font-family: "sans";
+}
+
+.outputnobb {
+	border-bottom: none;
+}
+
+.outputli {
+	border-radius: 0 0 6px 6px;
+}
+
+label {
+	max-width: 96%;
+	font-weight: bold;
+	color: white;
+	background-color: #555555;
+	border-bottom: none;
+	border-radius: 6px 6px 0 0;
+	padding: 2px 6px 3px 6px;
+	display: inline-block;
+	margin: 0;
+}
+
+.cblab {
+	background-color: rgba(0, 0, 0, 0);
+	font-weight: normal;
+	display: table-cell;
+}
+
+.posthead, .errposthead {
+	font-weight: bold;
+	font-size: 12pt;
+	margin-bottom: 0;
+	border-bottom: none;
+	border-radius: 6px 6px 0 0;
+	color: white;
+	background-color: #555555;
+	padding: 3px 6px 3px 6px;
+	margin-top: 15px;
+}
+
+.errposthead {
+	background-color: red;
+}
+
+.horsep {
+	width: 100%;
+	height: 25px;
+}
+
+#footer, #almfooter {
+	width: 100%;
+	text-align: center;
+	font-size: 9pt;
+	margin: 3mm 0 0 0;
+}
+
+#almfooter {
+	font-size: 10.5pt;
+}

BIN
imgs/icon-16.png


BIN
imgs/icon-180.png


BIN
imgs/icon-192.png


BIN
imgs/icon-32.png


BIN
imgs/icon-512.png


BIN
imgs/ogimage.png


+ 205 - 0
index.php

@@ -0,0 +1,205 @@
+<?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';
+const SREPO='https://git.lattuga.net/pongrebio/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 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 won’t proceed with the check.</div>\n";
+	} elseif ($hcode[0]!='2') {
+		if ($hcode[0]=='5')
+			echo "<div class=\"error\">«{$purlhn}» returned a server error. {$usname} won’t 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} won’t 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 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";
+		elseif ($hcode[0]=='1')
+			echo "<div class=\"error\">«{$purlhn}» returned an unexpected and useless informational message. {$usname} won’t proceed with the check.</div>\n";
+		else
+			echo "<div class=\"error\">«{$purlhn}» returned an unexpected HTTP code. {$usname} won’t 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} won’t 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} doesn’t point to an ActivityPub post. {$usname} won’t 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,'echofun',true,false);
+			//echo preprint($rl);
+			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 ($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]);
+}
+
+function echofun($msg) {
+	echo $msg;
+}
+
+?>

+ 36 - 0
lib/ckratelimit.php

@@ -0,0 +1,36 @@
+<?php
+function ckratelimit($headers,$echofun,$onlyret=false,$verbose=false) {//$echofun has to be the name of a defined function to pass messages to
+	$ret=null;
+	if (is_array($headers)) {
+		//echo "ckratelimit: {$headers}: ".print_r($headers,true));
+		$buff=[];
+		array_shift($headers);
+		foreach ($headers as $header)
+			if (preg_match('/^([^:]+):(.*)$/Uu',$header,$matches)===1)
+				$buff[$matches[1]]=trim($matches[2]);
+		$headers=$buff;
+		//print_r($headers);
+		if (isset($headers['Date']) && isset($headers['X-RateLimit-Reset']) && isset($headers['X-RateLimit-Remaining'])) {
+			//Wed, 30 Mar 2022 21:27:22 GMT
+			$srvnow=strtotime($headers['Date']);
+			//2022-03-31T04:05:00.058705Z
+			$srvrlreset=strtotime($headers['X-RateLimit-Reset']);
+			$srvrlremain=$headers['X-RateLimit-Remaining'];
+			$secstoreset=$srvrlreset-$srvnow;
+			$ret=['remaining'=>$srvrlremain,'secstoreset'=>$secstoreset];
+			if ($onlyret)
+				return $ret;
+			if ($verbose) $echofun("ckratelimit: X-RateLimit-Remaining: {$srvrlremain}; server time: {$srvnow}: ".gmdate('c',$srvnow).'; X-RateLimit-Reset: '.gmdate('c',$srvrlreset).'; current seconds before reset: '.$secstoreset.".\n");
+			if ($srvrlremain==0) {
+				$echofun("Reached rate limit, waiting {$secstoreset} seconds for rate limit reset ...\n");
+				sleep($secstoreset);
+			}
+		} else {
+			if ($verbose) $echofun("ckratelimit: no «Date» / «X-RateLimit-Reset» / «X-RateLimit-Remaining» header(s)!\n");
+		}
+	} else {
+		if ($verbose) $echofun("ckratelimit: headers is not an array!\n");
+	}
+	return $ret;
+}
+?>

+ 50 - 0
lib/ght.php

@@ -0,0 +1,50 @@
+<?php
+
+function ght($ts,$fa=null,$sd=2) {
+/*
+$ts is seconds (can be float)
+if not null, $fa has to be an array defining the output suffixes, see below
+	its default ;-)
+$sd is how many decimals to put after a dot after seconds (can be 0)
+*/
+	if ($fa==null)
+		$fa=[' year, ',' years, ',' week, ',' weeks, ',' day, ',' days, ',' hour, ',' hours, ',' minute, ',' minutes, ',' second',' seconds'];
+	$out='';
+	$i=0;
+// years
+	$x=floor($ts/31536000);
+	if ($x>0)
+		($x==1) ? $out.=$x.$fa[$i] : $out.=$x.$fa[$i+1];
+	$ts=$ts-$x*31536000;
+	$i+=2;
+// weeks
+	$x=floor($ts/604800);
+	if ($x>0)
+		($x==1) ? $out.=$x.$fa[$i] : $out.=$x.$fa[$i+1];
+	$ts=$ts-$x*604800;
+	$i+=2;
+// days
+	$x=floor($ts/86400);
+	if ($x>0)
+		($x==1) ? $out.=$x.$fa[$i] : $out.=$x.$fa[$i+1];
+	$ts=$ts-$x*86400;
+	$i+=2;
+// hours
+	$x=floor($ts/3600);
+	if ($x>0)
+		($x==1) ? $out.=$x.$fa[$i] : $out.=$x.$fa[$i+1];
+	$ts=$ts-$x*3600;
+	$i+=2;
+// minutes
+	$x=floor($ts/60);
+	if ($x>0)
+		($x==1) ? $out.=$x.$fa[$i] : $out.=$x.$fa[$i+1];
+	$ts=$ts-$x*60;
+	$i+=2;
+// seconds
+	$x=round($ts,$sd);
+	($x==1) ? $out.=$x.$fa[$i] : $out.=$x.$fa[$i+1];
+	return $out;
+}
+
+?>

+ 2 - 0
sec/.htaccess

@@ -0,0 +1,2 @@
+Order deny,allow
+Deny from all