[PLUGIN][ActivityPub] Implement Actor Update

Diverse minor bug fixes
This commit is contained in:
Diogo Peralta Cordeiro 2021-12-05 03:11:08 +00:00
parent 9506909e7a
commit 9512890264
Signed by: diogo
GPG Key ID: 18D2D35001FBFAB0
6 changed files with 78 additions and 66 deletions

View File

@ -80,7 +80,7 @@ class Inbox extends Controller
$type = Model::jsonToType($body); $type = Model::jsonToType($body);
if ($type->has('actor') === false) { if ($type->has('actor') === false) {
$error('Actor not found in the request.'); return $error('Actor not found in the request.');
} }
try { try {
@ -88,10 +88,11 @@ class Inbox extends Controller
$actor = Actor::getById($ap_actor->getActorId()); $actor = Actor::getById($ap_actor->getActorId());
DB::flush(); DB::flush();
} catch (Exception $e) { } catch (Exception $e) {
$error('Invalid actor.'); return $error('Invalid actor.');
} }
$actor_public_key = ActivitypubRsa::getByActor($actor)->getPublicKey(); $activitypub_rsa = ActivitypubRsa::getByActor($actor);
$actor_public_key = $activitypub_rsa->getPublicKey();
$headers = $this->request->headers->all(); $headers = $this->request->headers->all();
// Flattify headers // Flattify headers
@ -101,7 +102,7 @@ class Inbox extends Controller
if (!isset($headers['signature'])) { if (!isset($headers['signature'])) {
Log::debug('ActivityPub Inbox: HTTP Signature: Missing Signature header.'); Log::debug('ActivityPub Inbox: HTTP Signature: Missing Signature header.');
$error('Missing Signature header.', 400); return $error('Missing Signature header.', 400);
// TODO: support other methods beyond HTTP Signatures // TODO: support other methods beyond HTTP Signatures
} }
@ -110,22 +111,25 @@ class Inbox extends Controller
Log::debug('ActivityPub Inbox: HTTP Signature Data: ' . print_r($signatureData, true)); Log::debug('ActivityPub Inbox: HTTP Signature Data: ' . print_r($signatureData, true));
if (isset($signatureData['error'])) { if (isset($signatureData['error'])) {
Log::debug('ActivityPub Inbox: HTTP Signature: ' . json_encode($signatureData, JSON_PRETTY_PRINT)); Log::debug('ActivityPub Inbox: HTTP Signature: ' . json_encode($signatureData, JSON_PRETTY_PRINT));
$error(json_encode($signatureData, JSON_PRETTY_PRINT), 400); return $error(json_encode($signatureData, JSON_PRETTY_PRINT), 400);
} }
list($verified, /*$headers*/) = HTTPSignature::verify($actor_public_key, $signatureData, $headers, $path, $body); [$verified, /*$headers*/] = HTTPSignature::verify($actor_public_key, $signatureData, $headers, $path, $body);
// If the signature fails verification the first time, update profile as it might have changed public key // If the signature fails verification the first time, update profile as it might have changed public key
if ($verified !== 1) { if ($verified !== 1) {
try { try {
$res = Explorer::get_remote_user_activity($ap_actor->getUri()); $res = Explorer::get_remote_user_activity($ap_actor->getUri());
if (is_null($res)) {
return $error('Invalid remote actor.');
}
} catch (Exception) { } catch (Exception) {
$error('Invalid remote actor.'); return $error('Invalid remote actor.');
} }
try { try {
$actor = ActivitypubActor::update_profile($ap_actor, $res); ActivitypubActor::update_profile($ap_actor, $actor, $activitypub_rsa, $res);
} catch (Exception) { } catch (Exception) {
$error('Failed to updated remote actor information.'); return $error('Failed to updated remote actor information.');
} }
[$verified, /*$headers*/] = HTTPSignature::verify($actor_public_key, $signatureData, $headers, $path, $body); [$verified, /*$headers*/] = HTTPSignature::verify($actor_public_key, $signatureData, $headers, $path, $body);
@ -134,7 +138,7 @@ class Inbox extends Controller
// If it still failed despite profile update // If it still failed despite profile update
if ($verified !== 1) { if ($verified !== 1) {
Log::debug('ActivityPub Inbox: HTTP Signature: Invalid signature.'); Log::debug('ActivityPub Inbox: HTTP Signature: Invalid signature.');
$error('Invalid signature.'); return $error('Invalid signature.');
} }
// HTTP signature checked out, make sure the "actor" of the activity matches that of the signature // HTTP signature checked out, make sure the "actor" of the activity matches that of the signature

View File

@ -34,6 +34,7 @@ namespace Plugin\ActivityPub\Entity;
use App\Core\Cache; use App\Core\Cache;
use App\Core\Entity; use App\Core\Entity;
use App\Core\Log; use App\Core\Log;
use App\Entity\Actor;
use Component\FreeNetwork\Util\Discovery; use Component\FreeNetwork\Util\Discovery;
use DateTimeInterface; use DateTimeInterface;
use Exception; use Exception;
@ -237,6 +238,18 @@ class ActivitypubActor extends Entity
} }
} }
/**
* @param ActivitypubActor $ap_actor
* @param Actor $actor
* @param ActivitypubRsa $activitypub_rsa
* @param string $res
* @throws Exception
*/
public static function update_profile(self &$ap_actor, Actor &$actor, ActivitypubRsa &$activitypub_rsa, string $res): void
{
\Plugin\ActivityPub\Util\Model\Actor::fromJson($res, ['objects' => ['ActivitypubActor' => &$ap_actor, 'Actor' => &$actor, 'ActivitypubRsa' => &$activitypub_rsa]]);
}
public static function schemaDef(): array public static function schemaDef(): array
{ {
return [ return [

View File

@ -55,7 +55,7 @@ use const JSON_UNESCAPED_SLASHES;
*/ */
class Explorer class Explorer
{ {
private array $discovered_actor_profiles = []; private array $discovered_activitypub_actor_profiles = [];
/** /**
* Shortcut function to get a single profile from its URL. * Shortcut function to get a single profile from its URL.
@ -104,7 +104,7 @@ class Explorer
} }
Log::debug('ActivityPub Explorer: Started now looking for ' . $url); Log::debug('ActivityPub Explorer: Started now looking for ' . $url);
$this->discovered_actor_profiles = []; $this->discovered_activitypub_actor_profiles = [];
return $this->_lookup($url, $grab_online); return $this->_lookup($url, $grab_online);
} }
@ -123,7 +123,7 @@ class Explorer
* @throws ServerExceptionInterface * @throws ServerExceptionInterface
* @throws TransportExceptionInterface * @throws TransportExceptionInterface
* *
* @return array of Profile objects * @return array of ActivityPub Actor objects
*/ */
private function _lookup(string $url, bool $grab_online = true): array private function _lookup(string $url, bool $grab_online = true): array
{ {
@ -135,7 +135,7 @@ class Explorer
throw new NoSuchActorException('Actor not found.'); throw new NoSuchActorException('Actor not found.');
} }
return $this->discovered_actor_profiles; return $this->discovered_activitypub_actor_profiles;
} }
/** /**
@ -160,7 +160,7 @@ class Explorer
Log::debug('ActivityPub Explorer: Found a known Aprofile for ' . $uri); Log::debug('ActivityPub Explorer: Found a known Aprofile for ' . $uri);
// We found something! // We found something!
$this->discovered_actor_profiles[] = $aprofile; $this->discovered_activitypub_actor_profiles[] = $aprofile;
return true; return true;
} else { } else {
Log::debug('ActivityPub Explorer: Unable to find a known Aprofile for ' . $uri); Log::debug('ActivityPub Explorer: Unable to find a known Aprofile for ' . $uri);
@ -204,7 +204,7 @@ class Explorer
return true; return true;
} else { } else {
try { try {
$this->discovered_actor_profiles[] = Model\Actor::fromJson(json_encode($res)); $this->discovered_activitypub_actor_profiles[] = Model\Actor::fromJson(json_encode($res));
return true; return true;
} catch (Exception $e) { } catch (Exception $e) {
Log::debug( Log::debug(
@ -219,19 +219,6 @@ class Explorer
return false; return false;
} }
/**
* Validates a remote response in order to determine whether this
* response is a valid profile or not
*
* @param array $res remote response
*
* @return bool success state
*/
public static function validate_remote_response(array $res): bool
{
return !(!isset($res['id'], $res['preferredUsername'], $res['inbox'], $res['publicKey']['publicKeyPem']));
}
/** /**
* Get a ActivityPub Profile from it's uri * Get a ActivityPub Profile from it's uri
* *
@ -288,28 +275,20 @@ class Explorer
* @throws RedirectionExceptionInterface * @throws RedirectionExceptionInterface
* @throws ServerExceptionInterface * @throws ServerExceptionInterface
* @throws TransportExceptionInterface * @throws TransportExceptionInterface
* @throws Exception
* *
* @return array|false If it is able to fetch, false if it's gone * @return string|null If it is able to fetch, false if it's gone
* // Exceptions when network issues or unsupported Activity format * // Exceptions when network issues or unsupported Activity format
*/ */
public static function get_remote_user_activity(string $url): bool|array public static function get_remote_user_activity(string $url): string|null
{ {
$response = HTTPClient::get($url, ['headers' => ACTIVITYPUB::HTTP_CLIENT_HEADERS]); $response = HTTPClient::get($url, ['headers' => ACTIVITYPUB::HTTP_CLIENT_HEADERS]);
// If it was deleted // If it was deleted
if ($response->getStatusCode() == 410) { if ($response->getStatusCode() == 410) {
return false; return null;
} elseif (!HTTPClient::statusCodeIsOkay($response)) { // If it is unavailable } elseif (!HTTPClient::statusCodeIsOkay($response)) { // If it is unavailable
throw new Exception('Non Ok Status Code for given Actor URL.'); throw new Exception('Non Ok Status Code for given Actor URL.');
} }
$res = json_decode($response->getContent(), true); return $response->getContent();
if (is_null($res)) {
Log::debug('ActivityPub Explorer: Invalid JSON returned from given Actor URL: ' . $response->getContent());
throw new Exception('Given Actor URL didn\'t return a valid JSON.');
}
if (self::validate_remote_response($res)) {
Log::debug('ActivityPub Explorer: Found a valid remote actor for ' . $url);
return $res;
}
throw new Exception('ActivityPub Explorer: Failed to get activity.');
} }
} }

View File

@ -66,6 +66,7 @@ class Activity extends Model
public static function fromJson(string|AbstractObject $json, array $options = []): ActivitypubActivity public static function fromJson(string|AbstractObject $json, array $options = []): ActivitypubActivity
{ {
$type_activity = is_string($json) ? self::jsonToType($json) : $json; $type_activity = is_string($json) ? self::jsonToType($json) : $json;
$source = $options['source'];
$activity_stream_two_verb_to_gs_verb = fn(string $verb): string => match ($verb) { $activity_stream_two_verb_to_gs_verb = fn(string $verb): string => match ($verb) {
'Create' => 'create', 'Create' => 'create',
@ -82,15 +83,15 @@ class Activity extends Model
$actor = ActivityPub::getActorByUri($type_activity->get('actor')); $actor = ActivityPub::getActorByUri($type_activity->get('actor'));
// Store Object // Store Object
$obj = null; $obj = null;
if (!$type_activity->has('object') || !isset($type_activity->get('object')['type'])) { if (!$type_activity->has('object') || !$type_activity->get('object')->has('type')) {
throw new InvalidArgumentException('Activity Object or Activity Object Type is missing.'); throw new InvalidArgumentException('Activity Object or Activity Object Type is missing.');
} }
switch ($type_activity->get('object')['type']) { switch ($type_activity->get('object')->get('type')) {
case 'Note': case 'Note':
$obj = Note::toJson($type_activity->get('object'), ['source' => $source, 'actor_uri' => $type_activity->get('actor'), 'actor_id' => $actor->getId()]); $obj = Note::fromJson($type_activity->get('object'), ['source' => $source, 'actor_uri' => $type_activity->get('actor'), 'actor_id' => $actor->getId()]);
break; break;
default: default:
if (!Event::handle('ActivityPubObject', [$type_activity->get('object')['type'], $type_activity->get('object'), &$obj])) { if (!Event::handle('ActivityPubObject', [$type_activity->get('object')->get('type'), $type_activity->get('object'), &$obj])) {
throw new ClientException('Unsupported Object type.'); throw new ClientException('Unsupported Object type.');
} }
break; break;
@ -100,20 +101,20 @@ class Activity extends Model
$act = GSActivity::create([ $act = GSActivity::create([
'actor_id' => $actor->getId(), 'actor_id' => $actor->getId(),
'verb' => $activity_stream_two_verb_to_gs_verb($type_activity->get('type')), 'verb' => $activity_stream_two_verb_to_gs_verb($type_activity->get('type')),
'object_type' => $activity_stream_two_object_type_to_gs_table($type_activity->get('object')['type']), 'object_type' => $activity_stream_two_object_type_to_gs_table($type_activity->get('object')->get('type')),
'object_id' => $obj->getId(), 'object_id' => $obj->getId(),
'is_local' => false, 'is_local' => false,
'created' => new DateTime($activity['published'] ?? 'now'), 'created' => new DateTime($type_activity->get('published') ?? 'now'),
'source' => $source, 'source' => $source,
]); ]);
DB::persist($act); DB::persist($act);
// Store ActivityPub Activity // Store ActivityPub Activity
$ap_act = ActivitypubActivity::create([ $ap_act = ActivitypubActivity::create([
'activity_id' => $act->getId(), 'activity_id' => $act->getId(),
'activity_uri' => $activity['id'], 'activity_uri' => $type_activity->get('id'),
'object_uri' => $activity['object']['id'], 'object_uri' => $type_activity->get('object')->get('id'),
'is_local' => false, 'is_local' => false,
'created' => new DateTime($activity['published'] ?? 'now'), 'created' => new DateTime($type_activity->get('published') ?? 'now'),
'modified' => new DateTime(), 'modified' => new DateTime(),
]); ]);
DB::persist($ap_act); DB::persist($ap_act);

View File

@ -64,10 +64,10 @@ class Actor extends Model
* *
* @param string|AbstractObject $json * @param string|AbstractObject $json
* @param array $options * @param array $options
* @return GSActor * @return ActivitypubActor
* @throws Exception * @throws Exception
*/ */
public static function fromJson(string|AbstractObject $json, array $options = []): GSActor public static function fromJson(string|AbstractObject $json, array $options = []): ActivitypubActor
{ {
$person = is_string($json) ? self::jsonToType($json) : $json; $person = is_string($json) ? self::jsonToType($json) : $json;
@ -81,32 +81,39 @@ class Actor extends Model
'modified' => new DateTime(), 'modified' => new DateTime(),
]; ];
$actor = new GSActor(); $actor = $options['objects']['Actor'] ?? new GSActor();
foreach ($actor_map as $prop => $val) { foreach ($actor_map as $prop => $val) {
$set = Formatting::snakeCaseToCamelCase("set_{$prop}"); $set = Formatting::snakeCaseToCamelCase("set_{$prop}");
$actor->{$set}($val); $actor->{$set}($val);
} }
DB::persist($actor); if (!isset($options['objects']['Actor'])) {
DB::persist($actor);
}
// ActivityPub Actor // ActivityPub Actor
$aprofile = ActivitypubActor::create([ $ap_actor = ActivitypubActor::create([
'inbox_uri' => $person->get('inbox'), 'inbox_uri' => $person->get('inbox'),
'inbox_shared_uri' => ($person->has('endpoints') && isset($person->get('endpoints')['sharedInbox'])) ? $person->get('endpoints')['sharedInbox'] : null, 'inbox_shared_uri' => ($person->has('endpoints') && isset($person->get('endpoints')['sharedInbox'])) ? $person->get('endpoints')['sharedInbox'] : null,
'uri' => $person->get('id'), 'uri' => $person->get('id'),
'actor_id' => $actor->getId(), 'actor_id' => $actor->getId(),
'url' => $person->get('url') ?? null, 'url' => $person->get('url') ?? null,
]); ], $options['objects']['ActivitypubActor'] ?? null);
DB::persist($aprofile); if (!isset($options['objects']['ActivitypubActor'])) {
DB::persist($ap_actor);
}
// Public Key // Public Key
$apRSA = ActivitypubRsa::create([ $apRSA = ActivitypubRsa::create([
'actor_id' => $actor->getID(), 'actor_id' => $actor->getID(),
'public_key' => ($person->has('publicKey') && isset($person->get('publicKey')['publicKeyPem'])) ? $person->get('publicKey')['publicKeyPem'] : null, 'public_key' => ($person->has('publicKey') && isset($person->get('publicKey')['publicKeyPem'])) ? $person->get('publicKey')['publicKeyPem'] : null,
]); ], $options['objects']['ActivitypubRsa'] ?? null);
DB::persist($apRSA); if (!isset($options['objects']['ActivitypubRsa'])) {
DB::persist($apRSA);
}
// Avatar // Avatar
//if (isset($res['icon']['url'])) { //if (isset($res['icon']['url'])) {
@ -118,8 +125,8 @@ class Actor extends Model
// } // }
//} //}
Event::handle('ActivityPubNewActor', [&$aprofile, &$actor, &$apRSA]); Event::handle('ActivityPubNewActor', [&$ap_actor, &$actor, &$apRSA]);
return $aprofile; return $ap_actor;
} }
/** /**

View File

@ -58,11 +58,18 @@ abstract class Entity
public static function create(array $args, $obj = null) public static function create(array $args, $obj = null)
{ {
$class = static::class; $class = static::class;
$obj = $obj ?: new $class();
$date = new DateTime(); $date = new DateTime();
foreach (['created', 'modified'] as $prop) { if (!is_null($obj)) { // Update modified
if (property_exists($class, $prop)) { if (property_exists($class, 'modified')) {
$args[$prop] = $date; $args['modified'] = $date;
}
} else {
$obj = new $class();
foreach (['created', 'modified'] as $prop) {
if (property_exists($class, $prop)) {
$args[$prop] = $date;
}
} }
} }
@ -75,6 +82,7 @@ abstract class Entity
throw new InvalidArgumentException($m); throw new InvalidArgumentException($m);
} }
} }
return $obj; return $obj;
} }