Merge pull request #6071 from annando/ap-fetch-signed
Support for fetching non-public content / preparations for forum posts
This commit is contained in:
commit
62761e1099
5 changed files with 188 additions and 30 deletions
|
@ -7,6 +7,7 @@ namespace Friendica\Protocol;
|
||||||
use Friendica\Util\Network;
|
use Friendica\Util\Network;
|
||||||
use Friendica\Core\Protocol;
|
use Friendica\Core\Protocol;
|
||||||
use Friendica\Model\APContact;
|
use Friendica\Model\APContact;
|
||||||
|
use Friendica\Util\HTTPSignature;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief ActivityPub Protocol class
|
* @brief ActivityPub Protocol class
|
||||||
|
@ -59,11 +60,16 @@ class ActivityPub
|
||||||
/**
|
/**
|
||||||
* Fetches ActivityPub content from the given url
|
* Fetches ActivityPub content from the given url
|
||||||
*
|
*
|
||||||
* @param string $url content url
|
* @param string $url content url
|
||||||
|
* @param integer $uid User ID for the signature
|
||||||
* @return array
|
* @return array
|
||||||
*/
|
*/
|
||||||
public static function fetchContent($url)
|
public static function fetchContent($url, $uid = 0)
|
||||||
{
|
{
|
||||||
|
if (!empty($uid)) {
|
||||||
|
return HTTPSignature::fetch($url, 1);
|
||||||
|
}
|
||||||
|
|
||||||
$curlResult = Network::curl($url, false, $redirects, ['accept_content' => 'application/activity+json, application/ld+json']);
|
$curlResult = Network::curl($url, false, $redirects, ['accept_content' => 'application/activity+json, application/ld+json']);
|
||||||
if (!$curlResult->isSuccess() || empty($curlResult->getBody())) {
|
if (!$curlResult->isSuccess() || empty($curlResult->getBody())) {
|
||||||
return false;
|
return false;
|
||||||
|
|
|
@ -301,7 +301,9 @@ class Processor
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
$object = ActivityPub::fetchContent($url);
|
$uid = ActivityPub\Receiver::getFirstUserFromReceivers($child['receiver']);
|
||||||
|
|
||||||
|
$object = ActivityPub::fetchContent($url, $uid);
|
||||||
if (empty($object)) {
|
if (empty($object)) {
|
||||||
Logger::log('Activity ' . $url . ' was not fetchable, aborting.');
|
Logger::log('Activity ' . $url . ' was not fetchable, aborting.');
|
||||||
return;
|
return;
|
||||||
|
|
|
@ -111,12 +111,13 @@ class Receiver
|
||||||
/**
|
/**
|
||||||
* Fetches the object type for a given object id
|
* Fetches the object type for a given object id
|
||||||
*
|
*
|
||||||
* @param array $activity
|
* @param array $activity
|
||||||
* @param string $object_id Object ID of the the provided object
|
* @param string $object_id Object ID of the the provided object
|
||||||
|
* @param integer $uid User ID
|
||||||
*
|
*
|
||||||
* @return string with object type
|
* @return string with object type
|
||||||
*/
|
*/
|
||||||
private static function fetchObjectType($activity, $object_id)
|
private static function fetchObjectType($activity, $object_id, $uid = 0)
|
||||||
{
|
{
|
||||||
if (!empty($activity['as:object'])) {
|
if (!empty($activity['as:object'])) {
|
||||||
$object_type = JsonLD::fetchElement($activity['as:object'], '@type');
|
$object_type = JsonLD::fetchElement($activity['as:object'], '@type');
|
||||||
|
@ -135,7 +136,7 @@ class Receiver
|
||||||
return 'as:' . $profile['type'];
|
return 'as:' . $profile['type'];
|
||||||
}
|
}
|
||||||
|
|
||||||
$data = ActivityPub::fetchContent($object_id);
|
$data = ActivityPub::fetchContent($object_id, $uid);
|
||||||
if (!empty($data)) {
|
if (!empty($data)) {
|
||||||
$object = JsonLD::compact($data);
|
$object = JsonLD::compact($data);
|
||||||
$type = JsonLD::fetchElement($object, '@type');
|
$type = JsonLD::fetchElement($object, '@type');
|
||||||
|
@ -171,12 +172,15 @@ class Receiver
|
||||||
|
|
||||||
// When it is a delivery to a personal inbox we add that user to the receivers
|
// When it is a delivery to a personal inbox we add that user to the receivers
|
||||||
if (!empty($uid)) {
|
if (!empty($uid)) {
|
||||||
$owner = User::getOwnerDataById($uid);
|
|
||||||
$additional = ['uid:' . $uid => $uid];
|
$additional = ['uid:' . $uid => $uid];
|
||||||
$receivers = array_merge($receivers, $additional);
|
$receivers = array_merge($receivers, $additional);
|
||||||
|
} else {
|
||||||
|
// We possibly need some user to fetch private content,
|
||||||
|
// so we fetch the first out ot the list.
|
||||||
|
$uid = self::getFirstUserFromReceivers($receivers);
|
||||||
}
|
}
|
||||||
|
|
||||||
Logger::log('Receivers: ' . json_encode($receivers), Logger::DEBUG);
|
Logger::log('Receivers: ' . $uid . ' - ' . json_encode($receivers), Logger::DEBUG);
|
||||||
|
|
||||||
$object_id = JsonLD::fetchElement($activity, 'as:object');
|
$object_id = JsonLD::fetchElement($activity, 'as:object');
|
||||||
if (empty($object_id)) {
|
if (empty($object_id)) {
|
||||||
|
@ -184,14 +188,14 @@ class Receiver
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
$object_type = self::fetchObjectType($activity, $object_id);
|
$object_type = self::fetchObjectType($activity, $object_id, $uid);
|
||||||
|
|
||||||
// Fetch the content only on activities where this matters
|
// Fetch the content only on activities where this matters
|
||||||
if (in_array($type, ['as:Create', 'as:Update', 'as:Announce'])) {
|
if (in_array($type, ['as:Create', 'as:Update', 'as:Announce'])) {
|
||||||
if ($type == 'as:Announce') {
|
if ($type == 'as:Announce') {
|
||||||
$trust_source = false;
|
$trust_source = false;
|
||||||
}
|
}
|
||||||
$object_data = self::fetchObject($object_id, $activity['as:object'], $trust_source);
|
$object_data = self::fetchObject($object_id, $activity['as:object'], $trust_source, $uid);
|
||||||
if (empty($object_data)) {
|
if (empty($object_data)) {
|
||||||
Logger::log("Object data couldn't be processed", Logger::DEBUG);
|
Logger::log("Object data couldn't be processed", Logger::DEBUG);
|
||||||
return [];
|
return [];
|
||||||
|
@ -216,7 +220,7 @@ class Receiver
|
||||||
|
|
||||||
// An Undo is done on the object of an object, so we need that type as well
|
// An Undo is done on the object of an object, so we need that type as well
|
||||||
if ($type == 'as:Undo') {
|
if ($type == 'as:Undo') {
|
||||||
$object_data['object_object_type'] = self::fetchObjectType([], $object_data['object_object']);
|
$object_data['object_object_type'] = self::fetchObjectType([], $object_data['object_object'], $uid);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -235,6 +239,22 @@ class Receiver
|
||||||
return $object_data;
|
return $object_data;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetches the first uider id from the receiver array
|
||||||
|
*
|
||||||
|
* @param array $receivers Array with receivers
|
||||||
|
* @return integer user id;
|
||||||
|
*/
|
||||||
|
public static function getFirstUserFromReceivers($receivers)
|
||||||
|
{
|
||||||
|
foreach ($receivers as $receiver) {
|
||||||
|
if (!empty($receiver)) {
|
||||||
|
return $receiver;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Store the unprocessed data into the conversation table
|
* Store the unprocessed data into the conversation table
|
||||||
* This has to be done outside the regular function,
|
* This has to be done outside the regular function,
|
||||||
|
@ -395,10 +415,11 @@ class Receiver
|
||||||
*
|
*
|
||||||
* @param array $activity
|
* @param array $activity
|
||||||
* @param string $actor
|
* @param string $actor
|
||||||
|
* @param array $tags
|
||||||
*
|
*
|
||||||
* @return array with receivers (user id)
|
* @return array with receivers (user id)
|
||||||
*/
|
*/
|
||||||
private static function getReceivers($activity, $actor)
|
private static function getReceivers($activity, $actor, $tags = [])
|
||||||
{
|
{
|
||||||
$receivers = [];
|
$receivers = [];
|
||||||
|
|
||||||
|
@ -446,24 +467,34 @@ class Receiver
|
||||||
}
|
}
|
||||||
|
|
||||||
if (in_array($receiver, [$followers, self::PUBLIC_COLLECTION]) && !empty($actor)) {
|
if (in_array($receiver, [$followers, self::PUBLIC_COLLECTION]) && !empty($actor)) {
|
||||||
$networks = [Protocol::ACTIVITYPUB, Protocol::DFRN, Protocol::DIASPORA, Protocol::OSTATUS];
|
$receivers = array_merge($receivers, self::getReceiverForActor($actor, $tags));
|
||||||
$condition = ['nurl' => normalise_link($actor), 'rel' => [Contact::SHARING, Contact::FRIEND],
|
|
||||||
'network' => $networks, 'archive' => false, 'pending' => false];
|
|
||||||
$contacts = DBA::select('contact', ['uid'], $condition);
|
|
||||||
while ($contact = DBA::fetch($contacts)) {
|
|
||||||
if ($contact['uid'] != 0) {
|
|
||||||
$receivers['uid:' . $contact['uid']] = $contact['uid'];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
DBA::close($contacts);
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Fetching all directly addressed receivers
|
||||||
$condition = ['self' => true, 'nurl' => normalise_link($receiver)];
|
$condition = ['self' => true, 'nurl' => normalise_link($receiver)];
|
||||||
$contact = DBA::selectFirst('contact', ['uid'], $condition);
|
$contact = DBA::selectFirst('contact', ['uid', 'contact-type'], $condition);
|
||||||
if (!DBA::isResult($contact)) {
|
if (!DBA::isResult($contact)) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check if the potential receiver is following the actor
|
||||||
|
// Exception: The receiver is targetted via "to" or this is a comment
|
||||||
|
if ((($element != 'as:to') && empty($replyto)) || ($contact['contact-type'] == Contact::ACCOUNT_TYPE_COMMUNITY)) {
|
||||||
|
$networks = [Protocol::ACTIVITYPUB, Protocol::DFRN, Protocol::DIASPORA, Protocol::OSTATUS];
|
||||||
|
$condition = ['nurl' => normalise_link($actor), 'rel' => [Contact::SHARING, Contact::FRIEND],
|
||||||
|
'network' => $networks, 'archive' => false, 'pending' => false, 'uid' => $contact['uid']];
|
||||||
|
|
||||||
|
// Forum posts are only accepted from forum contacts
|
||||||
|
if ($contact['contact-type'] == Contact::ACCOUNT_TYPE_COMMUNITY) {
|
||||||
|
$condition['rel'] = [Contact::SHARING, Contact::FRIEND, Contact::FOLLOWER];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!DBA::exists('contact', $condition)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
$receivers['uid:' . $contact['uid']] = $contact['uid'];
|
$receivers['uid:' . $contact['uid']] = $contact['uid'];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -473,6 +504,71 @@ class Receiver
|
||||||
return $receivers;
|
return $receivers;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetch the receiver list of a given actor
|
||||||
|
*
|
||||||
|
* @param string $actor
|
||||||
|
* @param array $tags
|
||||||
|
*
|
||||||
|
* @return array with receivers (user id)
|
||||||
|
*/
|
||||||
|
public static function getReceiverForActor($actor, $tags)
|
||||||
|
{
|
||||||
|
$receivers = [];
|
||||||
|
$networks = [Protocol::ACTIVITYPUB, Protocol::DFRN, Protocol::DIASPORA, Protocol::OSTATUS];
|
||||||
|
$condition = ['nurl' => normalise_link($actor), 'rel' => [Contact::SHARING, Contact::FRIEND, Contact::FOLLOWER],
|
||||||
|
'network' => $networks, 'archive' => false, 'pending' => false];
|
||||||
|
$contacts = DBA::select('contact', ['uid', 'rel'], $condition);
|
||||||
|
while ($contact = DBA::fetch($contacts)) {
|
||||||
|
if (self::isValidReceiverForActor($contact, $actor, $tags)) {
|
||||||
|
$receivers['uid:' . $contact['uid']] = $contact['uid'];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
DBA::close($contacts);
|
||||||
|
return $receivers;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tests if the contact is a valid receiver for this actor
|
||||||
|
*
|
||||||
|
* @param array $contact
|
||||||
|
* @param string $actor
|
||||||
|
* @param array $tags
|
||||||
|
*
|
||||||
|
* @return array with receivers (user id)
|
||||||
|
*/
|
||||||
|
private static function isValidReceiverForActor($contact, $actor, $tags)
|
||||||
|
{
|
||||||
|
// Public contacts are no valid receiver
|
||||||
|
if ($contact['uid'] == 0) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Are we following the contact? Then this is a valid receiver
|
||||||
|
if (in_array($contact['rel'], [Contact::SHARING, Contact::FRIEND])) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// When the possible receiver isn't a community, then it is no valid receiver
|
||||||
|
$owner = User::getOwnerDataById($contact['uid']);
|
||||||
|
if (empty($owner) || ($owner['contact-type'] != Contact::ACCOUNT_TYPE_COMMUNITY)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Is the community account tagged?
|
||||||
|
foreach ($tags as $tag) {
|
||||||
|
if ($tag['type'] != 'Mention') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($tag['href'] == $owner['url']) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Switches existing contacts to ActivityPub
|
* Switches existing contacts to ActivityPub
|
||||||
*
|
*
|
||||||
|
@ -559,16 +655,17 @@ class Receiver
|
||||||
* @param string $object_id Object ID of the the provided object
|
* @param string $object_id Object ID of the the provided object
|
||||||
* @param array $object The provided object array
|
* @param array $object The provided object array
|
||||||
* @param boolean $trust_source Do we trust the provided object?
|
* @param boolean $trust_source Do we trust the provided object?
|
||||||
|
* @param integer $uid User ID for the signature that we use to fetch data
|
||||||
*
|
*
|
||||||
* @return array with trusted and valid object data
|
* @return array with trusted and valid object data
|
||||||
*/
|
*/
|
||||||
private static function fetchObject($object_id, $object = [], $trust_source = false)
|
private static function fetchObject($object_id, $object = [], $trust_source = false, $uid = 0)
|
||||||
{
|
{
|
||||||
// By fetching the type we check if the object is complete.
|
// By fetching the type we check if the object is complete.
|
||||||
$type = JsonLD::fetchElement($object, '@type');
|
$type = JsonLD::fetchElement($object, '@type');
|
||||||
|
|
||||||
if (!$trust_source || empty($type)) {
|
if (!$trust_source || empty($type)) {
|
||||||
$data = ActivityPub::fetchContent($object_id);
|
$data = ActivityPub::fetchContent($object_id, $uid);
|
||||||
if (!empty($data)) {
|
if (!empty($data)) {
|
||||||
$object = JsonLD::compact($data);
|
$object = JsonLD::compact($data);
|
||||||
Logger::log('Fetched content for ' . $object_id, Logger::DEBUG);
|
Logger::log('Fetched content for ' . $object_id, Logger::DEBUG);
|
||||||
|
@ -604,7 +701,7 @@ class Receiver
|
||||||
if (empty($object_id)) {
|
if (empty($object_id)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
return self::fetchObject($object_id);
|
return self::fetchObject($object_id, [], false, $uid);
|
||||||
}
|
}
|
||||||
|
|
||||||
Logger::log('Unhandled object type: ' . $type, Logger::DEBUG);
|
Logger::log('Unhandled object type: ' . $type, Logger::DEBUG);
|
||||||
|
@ -736,7 +833,7 @@ class Receiver
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
$object_data['receiver'] = self::getReceivers($object, $object_data['actor']);
|
$object_data['receiver'] = self::getReceivers($object, $object_data['actor'], $object_data['tags']);
|
||||||
|
|
||||||
// Common object data:
|
// Common object data:
|
||||||
|
|
||||||
|
|
|
@ -90,7 +90,7 @@ class HTTPSignature
|
||||||
$key = $key($sig_block['keyId']);
|
$key = $key($sig_block['keyId']);
|
||||||
}
|
}
|
||||||
|
|
||||||
Logger::log('Got keyID ' . $sig_block['keyId']);
|
Logger::log('Got keyID ' . $sig_block['keyId'], Logger::DEBUG);
|
||||||
|
|
||||||
if (!$key) {
|
if (!$key) {
|
||||||
return $result;
|
return $result;
|
||||||
|
@ -308,11 +308,59 @@ class HTTPSignature
|
||||||
$postResult = Network::post($target, $content, $headers);
|
$postResult = Network::post($target, $content, $headers);
|
||||||
$return_code = $postResult->getReturnCode();
|
$return_code = $postResult->getReturnCode();
|
||||||
|
|
||||||
Logger::log('Transmit to ' . $target . ' returned ' . $return_code);
|
Logger::log('Transmit to ' . $target . ' returned ' . $return_code, Logger::DEBUG);
|
||||||
|
|
||||||
return ($return_code >= 200) && ($return_code <= 299);
|
return ($return_code >= 200) && ($return_code <= 299);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Fetches JSON data for a user
|
||||||
|
*
|
||||||
|
* @param string $request request url
|
||||||
|
* @param integer $uid User id of the requester
|
||||||
|
*
|
||||||
|
* @return array JSON array
|
||||||
|
*/
|
||||||
|
public static function fetch($request, $uid)
|
||||||
|
{
|
||||||
|
$owner = User::getOwnerDataById($uid);
|
||||||
|
|
||||||
|
if (!$owner) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Header data that is about to be signed.
|
||||||
|
$host = parse_url($request, PHP_URL_HOST);
|
||||||
|
$path = parse_url($request, PHP_URL_PATH);
|
||||||
|
|
||||||
|
$headers = ['Host: ' . $host];
|
||||||
|
|
||||||
|
$signed_data = "(request-target): get " . $path . "\nhost: " . $host;
|
||||||
|
|
||||||
|
$signature = base64_encode(Crypto::rsaSign($signed_data, $owner['uprvkey'], 'sha256'));
|
||||||
|
|
||||||
|
$headers[] = 'Signature: keyId="' . $owner['url'] . '#main-key' . '",algorithm="rsa-sha256",headers="(request-target) host",signature="' . $signature . '"';
|
||||||
|
|
||||||
|
$headers[] = 'Accept: application/activity+json, application/ld+json';
|
||||||
|
|
||||||
|
$curlResult = Network::curl($request, false, $redirects, ['header' => $headers]);
|
||||||
|
$return_code = $curlResult->getReturnCode();
|
||||||
|
|
||||||
|
Logger::log('Fetched for user ' . $uid . ' from ' . $request . ' returned ' . $return_code, Logger::DEBUG);
|
||||||
|
|
||||||
|
if (!$curlResult->isSuccess() || empty($curlResult->getBody())) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$content = json_decode($curlResult->getBody(), true);
|
||||||
|
|
||||||
|
if (empty($content) || !is_array($content)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $content;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Gets a signer from a given HTTP request
|
* @brief Gets a signer from a given HTTP request
|
||||||
*
|
*
|
||||||
|
|
|
@ -83,6 +83,7 @@ class Network
|
||||||
* 'novalidate' => do not validate SSL certs, default is to validate using our CA list
|
* 'novalidate' => do not validate SSL certs, default is to validate using our CA list
|
||||||
* 'nobody' => only return the header
|
* 'nobody' => only return the header
|
||||||
* 'cookiejar' => path to cookie jar file
|
* 'cookiejar' => path to cookie jar file
|
||||||
|
* 'header' => header array
|
||||||
*
|
*
|
||||||
* @return CurlResult
|
* @return CurlResult
|
||||||
*/
|
*/
|
||||||
|
@ -136,6 +137,10 @@ class Network
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!empty($opts['header'])) {
|
||||||
|
curl_setopt($ch, CURLOPT_HTTPHEADER, $opts['header']);
|
||||||
|
}
|
||||||
|
|
||||||
@curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
|
@curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
|
||||||
@curl_setopt($ch, CURLOPT_USERAGENT, $a->getUserAgent());
|
@curl_setopt($ch, CURLOPT_USERAGENT, $a->getUserAgent());
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue