aboutsummaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
-rw-r--r--Digitigrade/Job/PushObject.php4
-rw-r--r--Digitigrade/Model/Instance.php2
-rw-r--r--Digitigrade/Policy.php36
-rw-r--r--Digitigrade/Policy/BlockInstance.php18
-rw-r--r--Digitigrade/Policy/RemoteInstanceInfoSubstitution.php33
-rw-r--r--Digitigrade/PolicyManager.php30
-rw-r--r--Digitigrade/Singleton.php1
-rw-r--r--policies.conf.php7
-rw-r--r--routes/actor.php9
-rw-r--r--routes/admin.php1
-rw-r--r--routes/interaction.php5
-rw-r--r--routes/note.php4
12 files changed, 146 insertions, 4 deletions
diff --git a/Digitigrade/Job/PushObject.php b/Digitigrade/Job/PushObject.php
index c3d7210..0278eb5 100644
--- a/Digitigrade/Job/PushObject.php
+++ b/Digitigrade/Job/PushObject.php
@@ -3,6 +3,7 @@ namespace Digitigrade\Job;
use Digitigrade\Exception\AuthException;
use Digitigrade\Exception\EndpointMissingException;
+use Digitigrade\HttpResponseStatus\PolicyRejected;
use Digitigrade\Job;
use Digitigrade\Logger;
use Digitigrade\Model\Instance;
@@ -38,6 +39,9 @@ class PushObject extends Job {
// start auth now and hopefully by the next retry we'll be able to
$instance->beginOutboundAuth();
throw $e;
+ } catch (PolicyRejected $e) {
+ $log->warning("can't push to $instance->domain because a policy rejected it. giving up");
+ // and don't throw again, pretty much same reasoning as above
}
}
} \ No newline at end of file
diff --git a/Digitigrade/Model/Instance.php b/Digitigrade/Model/Instance.php
index 5545849..eecc6ff 100644
--- a/Digitigrade/Model/Instance.php
+++ b/Digitigrade/Model/Instance.php
@@ -6,6 +6,7 @@ use Digitigrade\Exception\EndpointMissingException;
use Digitigrade\GlobalConfig;
use Digitigrade\HttpResponseStatus\Unauthorized;
use Digitigrade\Job\RefreshOutboundAuthToken;
+use Digitigrade\PolicyManager;
class Instance extends FetchableModel {
public ?int $id;
@@ -208,6 +209,7 @@ class Instance extends FetchableModel {
if (!$this->auth->outboundPushEnabled) {
throw new \RuntimeException("won't push to $this->domain because it hasn't subscribed to us");
}
+ PolicyManager::getInstance()->checkFederationOrThrow($obj, $this);
$context = stream_context_create(['http' => [
'method' => 'POST',
'header' => "Content-Type: application/json\nAuthorization: Bearer " . $this->auth->outboundToken,
diff --git a/Digitigrade/Policy.php b/Digitigrade/Policy.php
index 56fbdb0..973c229 100644
--- a/Digitigrade/Policy.php
+++ b/Digitigrade/Policy.php
@@ -2,6 +2,7 @@
namespace Digitigrade;
use Digitigrade\Model\Actor;
+use Digitigrade\Model\Instance;
use Digitigrade\Model\Interaction;
use Digitigrade\Model\Note;
@@ -34,4 +35,39 @@ abstract class Policy {
}
// TODO: public function checkExtension()
+
+ /**
+ * Checks if an Actor is allowed to be federated to a certain Instance,
+ * and possibly alters it.
+ * @param Actor $actor the actor to check (may be modified in-place)
+ * @param ?Instance $instance the instance that is receiving this (may be null if unknown)
+ * @return bool true if allowed, false if the request should be cancelled
+ */
+ public function checkActorFederation(Actor $actor, ?Instance $instance): bool {
+ return true;
+ }
+
+ /**
+ * Checks if an Interaction is allowed to be federated to a certain Instance,
+ * and possibly alters it.
+ * @param Interaction $interaction the interaction to check (may be modified in-place)
+ * @param ?Instance $instance the instance that is receiving this (may be null if unknown)
+ * @return bool true if allowed, false if the request should be cancelled
+ */
+ public function checkInteractionFederation(Interaction $interaction, ?Instance $instance): bool {
+ return true;
+ }
+
+ /**
+ * Checks if an Note is allowed to be federated to a certain Instance,
+ * and possibly alters it.
+ * @param Note $note the note to check (may be modified in-place)
+ * @param ?Instance $instance the instance that is receiving this (may be null if unknown)
+ * @return bool true if allowed, false if the request should be cancelled
+ */
+ public function checkNoteFederation(Note $note, ?Instance $instance): bool {
+ return true;
+ }
+
+ // TODO: public function checkExtensionFederation()
} \ No newline at end of file
diff --git a/Digitigrade/Policy/BlockInstance.php b/Digitigrade/Policy/BlockInstance.php
index 5f902f2..053418c 100644
--- a/Digitigrade/Policy/BlockInstance.php
+++ b/Digitigrade/Policy/BlockInstance.php
@@ -2,6 +2,7 @@
namespace Digitigrade\Policy;
use Digitigrade\Model\Actor;
+use Digitigrade\Model\Instance;
use Digitigrade\Model\Interaction;
use Digitigrade\Model\Note;
use Digitigrade\Model\PushableModel;
@@ -15,8 +16,11 @@ class BlockInstance extends Policy {
$this->blockedHosts = $hosts;
}
- private function isBlocked(PushableModel $obj): bool {
- $objHost = hostname_from_uri($obj->uri);
+ private function isBlocked(PushableModel|Instance|null $obj): bool {
+ if ($obj == null) {
+ return false;
+ }
+ $objHost = hostname_from_uri($obj instanceof Instance ? $obj->domain : $obj->uri);
foreach ($this->blockedHosts as $host) {
if (str_ends_with($objHost, $host)) {
return true;
@@ -34,4 +38,14 @@ class BlockInstance extends Policy {
public function checkNote(Note $note): bool {
return !$this->isBlocked($note);
}
+
+ public function checkActorFederation(Actor $actor, ?Instance $instance): bool {
+ return !$this->isBlocked($instance);
+ }
+ public function checkInteractionFederation(Interaction $interaction, ?Instance $instance): bool {
+ return !$this->isBlocked($instance);
+ }
+ public function checkNoteFederation(Note $note, ?Instance $instance): bool {
+ return !$this->isBlocked($instance);
+ }
} \ No newline at end of file
diff --git a/Digitigrade/Policy/RemoteInstanceInfoSubstitution.php b/Digitigrade/Policy/RemoteInstanceInfoSubstitution.php
new file mode 100644
index 0000000..cbcecfc
--- /dev/null
+++ b/Digitigrade/Policy/RemoteInstanceInfoSubstitution.php
@@ -0,0 +1,33 @@
+<?php
+namespace Digitigrade\Policy;
+
+use Digitigrade\Model\Instance;
+use Digitigrade\Model\Note;
+use Digitigrade\Policy;
+
+class RemoteInstanceInfoSubstitution extends Policy {
+ public function checkNoteFederation(Note $note, ?Instance $instance): bool {
+ if ($instance == null) {
+ return true;
+ }
+
+ $infos = [
+ '%Instance.domain' => $instance->domain,
+ '%Instance.name' => $instance->name,
+ '%Instance.description' => $instance->description ?? '',
+ '%Instance.softwareName' => $instance->softwareName,
+ '%Instance.softwareDescription' => $instance->softwareDescription ?? '',
+ '%Instance.softwareHomepage' => $instance->softwareHomepage ?? '',
+ '%Instance.softwareVersion' => $instance->softwareVersion ?? ''
+ ];
+
+ foreach ($infos as $needle => $replacement) {
+ $note->plainContent = str_replace($needle, $replacement, $note->plainContent);
+ foreach ($note->formattedContent as $type => $content) {
+ $note->formattedContent[$type] = str_replace($needle, $replacement, $content);
+ }
+ }
+
+ return true;
+ }
+} \ No newline at end of file
diff --git a/Digitigrade/PolicyManager.php b/Digitigrade/PolicyManager.php
index 24c2c7f..69e2ab0 100644
--- a/Digitigrade/PolicyManager.php
+++ b/Digitigrade/PolicyManager.php
@@ -1,8 +1,10 @@
<?php
namespace Digitigrade;
+use Digitigrade\HttpResponseStatus\PolicyRejected;
use Digitigrade\Model\Actor;
use Digitigrade\Model\FetchableModel;
+use Digitigrade\Model\Instance;
use Digitigrade\Model\Interaction;
use Digitigrade\Model\Note;
@@ -34,6 +36,34 @@ class PolicyManager extends Singleton {
};
}
+ public function checkOrThrow(FetchableModel $object) {
+ if (!$this->check($object)) {
+ throw new PolicyRejected();
+ }
+ }
+
+ /**
+ * Checks if an object is allowed to be sent to a certain Instance by all
+ * policies, and possibly alters it.
+ * @param FetchableModel $object the object to check (may be modified in-place)
+ * @param ?Instance $instance the instance it's being sent to (may be null if unknown)
+ * @return bool true if allowed, false if the request should be cancelled
+ */
+ public function checkFederation(FetchableModel $object, ?Instance $instance): bool {
+ return match ($object::class) {
+ Actor::class => self::arrayAll($this->policies, fn(Policy $p) => $p->checkActorFederation($object, $instance)),
+ Interaction::class => self::arrayAll($this->policies, fn(Policy $p) => $p->checkInteractionFederation($object, $instance)),
+ Note::class => self::arrayAll($this->policies, fn(Policy $p) => $p->checkNoteFederation($object, $instance)),
+ default => true
+ };
+ }
+
+ public function checkFederationOrThrow(FetchableModel $object, ?Instance $instance) {
+ if (!$this->checkFederation($object, $instance)) {
+ throw new PolicyRejected();
+ }
+ }
+
private static function arrayAll(array $arr, callable $test): bool {
// i would use array_all but that's php 8.4+ only which i don't have lol
foreach ($arr as $item) {
diff --git a/Digitigrade/Singleton.php b/Digitigrade/Singleton.php
index 23b2e78..a90bb4a 100644
--- a/Digitigrade/Singleton.php
+++ b/Digitigrade/Singleton.php
@@ -10,7 +10,6 @@ abstract class Singleton {
*/
public static function getInstance() {
if (!isset(self::$instances[static::class])) {
- //error_log('making a new ' . static::class);
self::$instances[static::class] = new static();
}
return self::$instances[static::class];
diff --git a/policies.conf.php b/policies.conf.php
index 389551a..503ca74 100644
--- a/policies.conf.php
+++ b/policies.conf.php
@@ -3,6 +3,13 @@
// Policy config file
Digitigrade\PolicyManager::getInstance()->registerAll(
+ // loads blocked instance domains from blocked-instances.txt
new Digitigrade\Policy\BlockInstance(...explode("\n", file_get_contents(__DIR__ . '/blocked-instances.txt'))),
+
+ // provides a very basic censorship / word blocking mechanism
//new Digitigrade\Policy\BasicCensor(['fuck', 'shit'], '••••'),
+
+ // replaces text like %Instance.domain in notes with the corresponding value when sending to remote instances
+ // e.g. users on social.example would see "hello %Instance.domain" as "hello social.example" on their end
+ //new Digitigrade\Policy\RemoteInstanceInfoSubstitution,
); \ No newline at end of file
diff --git a/routes/actor.php b/routes/actor.php
index c228f45..b768413 100644
--- a/routes/actor.php
+++ b/routes/actor.php
@@ -1,21 +1,26 @@
<?php
use Digitigrade\HttpResponseStatus\NotFound;
+use Digitigrade\HttpResponseStatus\PolicyRejected;
use Digitigrade\HttpResponseStatus\TemporaryRedirect;
use Digitigrade\HttpResponseStatus\Unauthorized;
use Digitigrade\Model\Actor;
+use Digitigrade\Model\Instance;
use Digitigrade\Model\Note;
use Digitigrade\Model\NotePrivacyScope;
+use Digitigrade\PolicyManager;
use Digitigrade\Router;
Router::getInstance()->mount('/actor/:id', function (array $args) {
$actor = Actor::find($args['id']);
+ $instance = Instance::findByRequestHeaders();
if ($actor == null || $actor->deleted) {
throw new NotFound("i don't know that actor");
}
if (!$actor->isLocal) {
throw new NotFound("that actor is not local!");
}
+ PolicyManager::getInstance()->checkFederationOrThrow($actor, $instance);
if (isset($_SERVER['HTTP_ACCEPT']) && str_contains($_SERVER['HTTP_ACCEPT'], 'text/html')) {
throw new TemporaryRedirect("/@/$actor->handle");
}
@@ -24,6 +29,8 @@ Router::getInstance()->mount('/actor/:id', function (array $args) {
Router::getInstance()->mount('/actor/:id/basicFeed', function (array $args) {
$actor = Actor::find($args['id']);
+ $instance = Instance::findByRequestHeaders();
+ PolicyManager::getInstance()->checkFederationOrThrow($actor, $instance);
$page = +($_GET['page'] ?? 1);
$pageSize = 100;
$notes = array_filter(
@@ -47,6 +54,8 @@ Router::getInstance()->mount('/actor/:id/basicFeed', function (array $args) {
Router::getInstance()->mount('/actor/:id/fullFeed', function (array $args) {
$actor = Actor::find($args['id']);
+ $instance = Instance::findByRequestHeaders();
+ PolicyManager::getInstance()->checkFederationOrThrow($actor, $instance);
$page = +($_GET['page'] ?? 1);
$pageSize = 10;
$notes = array_filter(
diff --git a/routes/admin.php b/routes/admin.php
index 9915fde..c89f3d5 100644
--- a/routes/admin.php
+++ b/routes/admin.php
@@ -18,7 +18,6 @@ Router::getInstance()->mount('/fragment/admin/settings/:prefix', function (array
foreach ($_POST as $key => $value) {
// same workaround as in settings_field
$key = str_replace('<DOT>', '.', $key);
- error_log("setting '$key' to '$value'");
$settings->set($key, $value);
}
$saved = true;
diff --git a/routes/interaction.php b/routes/interaction.php
index 53bc193..5768fe1 100644
--- a/routes/interaction.php
+++ b/routes/interaction.php
@@ -1,16 +1,21 @@
<?php
use Digitigrade\HttpResponseStatus\NotFound;
+use Digitigrade\HttpResponseStatus\PolicyRejected;
+use Digitigrade\Model\Instance;
use Digitigrade\Model\Interaction;
+use Digitigrade\PolicyManager;
use Digitigrade\Router;
Router::getInstance()->mount('/interaction/:id', function (array $args) {
$interaction = Interaction::find($args['id']);
+ $instance = Instance::findByRequestHeaders();
if ($interaction == null || $interaction->deleted) {
throw new NotFound("i don't know that interaction");
}
if (!$interaction->author->isLocal) {
throw new NotFound("i don't want to tell you about non-local interactions!");
}
+ PolicyManager::getInstance()->checkFederationOrThrow($interaction, $instance);
json_response($interaction);
}); \ No newline at end of file
diff --git a/routes/note.php b/routes/note.php
index 90009d2..2612f0f 100644
--- a/routes/note.php
+++ b/routes/note.php
@@ -2,21 +2,25 @@
use Digitigrade\HttpResponseStatus\Forbidden;
use Digitigrade\HttpResponseStatus\NotFound;
+use Digitigrade\HttpResponseStatus\PolicyRejected;
use Digitigrade\HttpResponseStatus\TemporaryRedirect;
use Digitigrade\Model\Instance;
use Digitigrade\Model\Note;
use Digitigrade\Model\NotePrivacyScope;
use Digitigrade\Model\UserAccount;
+use Digitigrade\PolicyManager;
use Digitigrade\Router;
Router::getInstance()->mount('/note/:id', function (array $args) {
$note = Note::find($args['id']);
+ $instance = Instance::findByRequestHeaders();
if ($note == null || $note->deleted) {
throw new NotFound("i don't know that note");
}
if (!$note->author->isLocal) {
throw new NotFound("i don't want to tell you about non local notes sorry");
}
+ PolicyManager::getInstance()->checkFederationOrThrow($note, $instance);
if (isset($_SERVER['HTTP_ACCEPT']) && str_contains($_SERVER['HTTP_ACCEPT'], 'text/html')) {
throw new TemporaryRedirect('/@/' . $note->author->handle . "/note/$note->id");
}