mirror of
https://github.com/pixelfed/pixelfed.git
synced 2024-11-25 15:55:22 +00:00
Add shared inbox
This commit is contained in:
parent
0904a9d02f
commit
4733ca9fb9
8 changed files with 231 additions and 90 deletions
|
@ -108,6 +108,17 @@ class FederationController extends Controller
|
|||
return;
|
||||
}
|
||||
|
||||
public function sharedInbox(Request $request)
|
||||
{
|
||||
abort_if(!config('federation.activitypub.enabled'), 404);
|
||||
abort_if(!config('federation.activitypub.sharedInbox'), 404);
|
||||
|
||||
$headers = $request->headers->all();
|
||||
$payload = $request->getContent();
|
||||
dispatch(new InboxWorker($headers, $payload))->onQueue('high');
|
||||
return;
|
||||
}
|
||||
|
||||
public function userFollowing(Request $request, $username)
|
||||
{
|
||||
abort_if(!config('federation.activitypub.enabled'), 404);
|
||||
|
|
|
@ -26,10 +26,9 @@ class InboxWorker implements ShouldQueue
|
|||
*
|
||||
* @return void
|
||||
*/
|
||||
public function __construct($headers, $profile, $payload)
|
||||
public function __construct($headers, $payload)
|
||||
{
|
||||
$this->headers = $headers;
|
||||
$this->profile = $profile;
|
||||
$this->payload = $payload;
|
||||
}
|
||||
|
||||
|
@ -40,6 +39,116 @@ class InboxWorker implements ShouldQueue
|
|||
*/
|
||||
public function handle()
|
||||
{
|
||||
(new Inbox($this->headers, $this->profile, $this->payload))->handle();
|
||||
$profile = null;
|
||||
$headers = $this->headers;
|
||||
$payload = json_decode($this->payload, true, 8);
|
||||
|
||||
if(!isset($headers['signature']) || !isset($headers['date'])) {
|
||||
return;
|
||||
}
|
||||
|
||||
if(empty($headers) || empty($payload)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if($this->verifySignature($headers, $payload) == true) {
|
||||
(new Inbox($headers, $profile, $payload))->handle();
|
||||
return;
|
||||
} else if($this->blindKeyRotation($headers, $payload) == true) {
|
||||
(new Inbox($headers, $profile, $payload))->handle();
|
||||
return;
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
protected function verifySignature($headers, $payload)
|
||||
{
|
||||
$body = $this->payload;
|
||||
$bodyDecoded = $payload;
|
||||
$signature = is_array($headers['signature']) ? $headers['signature'][0] : $headers['signature'];
|
||||
$date = is_array($headers['date']) ? $headers['date'][0] : $headers['date'];
|
||||
if(!$signature) {
|
||||
return;
|
||||
}
|
||||
if(!$date) {
|
||||
return;
|
||||
}
|
||||
if(!now()->parse($date)->gt(now()->subDays(1)) ||
|
||||
!now()->parse($date)->lt(now()->addDays(1))
|
||||
) {
|
||||
return;
|
||||
}
|
||||
$signatureData = HttpSignature::parseSignatureHeader($signature);
|
||||
$keyId = Helpers::validateUrl($signatureData['keyId']);
|
||||
$id = Helpers::validateUrl($bodyDecoded['id']);
|
||||
$keyDomain = parse_url($keyId, PHP_URL_HOST);
|
||||
$idDomain = parse_url($id, PHP_URL_HOST);
|
||||
if(isset($bodyDecoded['object'])
|
||||
&& is_array($bodyDecoded['object'])
|
||||
&& isset($bodyDecoded['object']['attributedTo'])
|
||||
) {
|
||||
if(parse_url($bodyDecoded['object']['attributedTo'], PHP_URL_HOST) !== $keyDomain) {
|
||||
return;
|
||||
abort(400, 'Invalid request');
|
||||
}
|
||||
}
|
||||
if(!$keyDomain || !$idDomain || $keyDomain !== $idDomain) {
|
||||
return;
|
||||
abort(400, 'Invalid request');
|
||||
}
|
||||
$actor = Profile::whereKeyId($keyId)->first();
|
||||
if(!$actor) {
|
||||
$actorUrl = is_array($bodyDecoded['actor']) ? $bodyDecoded['actor'][0] : $bodyDecoded['actor'];
|
||||
$actor = Helpers::profileFirstOrNew($actorUrl);
|
||||
}
|
||||
if(!$actor) {
|
||||
return;
|
||||
}
|
||||
$pkey = openssl_pkey_get_public($actor->public_key);
|
||||
$inboxPath = "/f/inbox";
|
||||
list($verified, $headers) = HttpSignature::verify($pkey, $signatureData, $headers, $inboxPath, $body);
|
||||
if($verified == 1) {
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
protected function blindKeyRotation($headers, $payload)
|
||||
{
|
||||
$signature = is_array($headers['signature']) ? $headers['signature'][0] : $headers['signature'];
|
||||
$date = is_array($headers['date']) ? $headers['date'][0] : $headers['date'];
|
||||
if(!$signature) {
|
||||
return;
|
||||
}
|
||||
if(!$date) {
|
||||
return;
|
||||
}
|
||||
if(!now()->parse($date)->gt(now()->subDays(1)) ||
|
||||
!now()->parse($date)->lt(now()->addDays(1))
|
||||
) {
|
||||
return;
|
||||
}
|
||||
$signatureData = HttpSignature::parseSignatureHeader($signature);
|
||||
$keyId = Helpers::validateUrl($signatureData['keyId']);
|
||||
$actor = Profile::whereKeyId($keyId)->whereNotNull('remote_url')->first();
|
||||
if(!$actor) {
|
||||
return;
|
||||
}
|
||||
if(Helpers::validateUrl($actor->remote_url) == false) {
|
||||
return;
|
||||
}
|
||||
$res = Zttp::timeout(5)->withHeaders([
|
||||
'Accept' => 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"',
|
||||
'User-Agent' => 'PixelfedBot v0.1 - https://pixelfed.org',
|
||||
])->get($actor->remote_url);
|
||||
$res = json_decode($res->body(), true, 8);
|
||||
if($res['publicKey']['id'] !== $actor->key_id) {
|
||||
return;
|
||||
}
|
||||
$actor->public_key = $res['publicKey']['publicKeyPem'];
|
||||
$actor->save();
|
||||
return $this->verifySignature($headers, $payload);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -44,6 +44,9 @@ class ProfileTransformer extends Fractal\TransformerAbstract
|
|||
'mediaType' => 'image/jpeg',
|
||||
'url' => $profile->avatarUrl(),
|
||||
],
|
||||
'endpoints' => [
|
||||
'sharedInbox' => config('app.url') . '/f/inbox'
|
||||
]
|
||||
];
|
||||
}
|
||||
}
|
||||
|
|
|
@ -135,41 +135,49 @@ class Helpers {
|
|||
if(is_array($url)) {
|
||||
$url = $url[0];
|
||||
}
|
||||
|
||||
$localhosts = [
|
||||
'127.0.0.1', 'localhost', '::1'
|
||||
];
|
||||
|
||||
if(mb_substr($url, 0, 8) !== 'https://') {
|
||||
return false;
|
||||
}
|
||||
$hash = hash('sha256', $url);
|
||||
$key = "helpers:url:valid:sha256-{$hash}";
|
||||
$ttl = now()->addMinutes(5);
|
||||
|
||||
$valid = filter_var($url, FILTER_VALIDATE_URL);
|
||||
$valid = Cache::remember($key, $ttl, function() use($url) {
|
||||
$localhosts = [
|
||||
'127.0.0.1', 'localhost', '::1'
|
||||
];
|
||||
|
||||
if(!$valid) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$host = parse_url($valid, PHP_URL_HOST);
|
||||
|
||||
if(count(dns_get_record($host, DNS_A | DNS_AAAA)) == 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if(config('costar.enabled') == true) {
|
||||
if(
|
||||
(config('costar.domain.block') != null && Str::contains($host, config('costar.domain.block')) == true) ||
|
||||
(config('costar.actor.block') != null && in_array($url, config('costar.actor.block')) == true)
|
||||
) {
|
||||
if(mb_substr($url, 0, 8) !== 'https://') {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if(in_array($host, $localhosts)) {
|
||||
return false;
|
||||
}
|
||||
$valid = filter_var($url, FILTER_VALIDATE_URL);
|
||||
|
||||
return $valid;
|
||||
if(!$valid) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$host = parse_url($valid, PHP_URL_HOST);
|
||||
|
||||
if(count(dns_get_record($host, DNS_A | DNS_AAAA)) == 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if(config('costar.enabled') == true) {
|
||||
if(
|
||||
(config('costar.domain.block') != null && Str::contains($host, config('costar.domain.block')) == true) ||
|
||||
(config('costar.actor.block') != null && in_array($url, config('costar.actor.block')) == true)
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if(in_array($host, $localhosts)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
});
|
||||
|
||||
return (bool) $valid;
|
||||
}
|
||||
|
||||
public static function validateLocalUrl($url)
|
||||
|
@ -194,19 +202,25 @@ class Helpers {
|
|||
];
|
||||
}
|
||||
|
||||
public static function fetchFromUrl($url)
|
||||
public static function fetchFromUrl($url = false)
|
||||
{
|
||||
$url = self::validateUrl($url);
|
||||
if($url == false) {
|
||||
if(self::validateUrl($url) == false) {
|
||||
return;
|
||||
}
|
||||
$res = Zttp::withHeaders(self::zttpUserAgent())->get($url);
|
||||
$res = json_decode($res->body(), true, 8);
|
||||
if(json_last_error() == JSON_ERROR_NONE) {
|
||||
return $res;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
|
||||
$hash = hash('sha256', $url);
|
||||
$key = "helpers:url:fetcher:sha256-{$hash}";
|
||||
$ttl = now()->addMinutes(5);
|
||||
|
||||
return Cache::remember($key, $ttl, function() use($url) {
|
||||
$res = Zttp::withoutVerifying()->withHeaders(self::zttpUserAgent())->get($url);
|
||||
$res = json_decode($res->body(), true, 8);
|
||||
if(json_last_error() == JSON_ERROR_NONE) {
|
||||
return $res;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public static function fetchProfileFromUrl($url)
|
||||
|
@ -444,6 +458,7 @@ class Helpers {
|
|||
$profile->name = isset($res['name']) ? Purify::clean($res['name']) : 'user';
|
||||
$profile->bio = isset($res['summary']) ? Purify::clean($res['summary']) : null;
|
||||
$profile->last_fetched_at = now();
|
||||
$profile->sharedInbox = isset($res['endpoints']) && isset($res['endpoints']['sharedInbox']) && Helpers::validateUrl($res['endpoints']['sharedInbox']) ? $res['endpoints']['sharedInbox'] : null;
|
||||
$profile->save();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -323,7 +323,7 @@ class Inbox
|
|||
public function handleFollowActivity()
|
||||
{
|
||||
$actor = $this->actorFirstOrCreate($this->payload['actor']);
|
||||
$target = $this->profile;
|
||||
$target = $this->actorFirstOrCreate($this->payload['object']);
|
||||
if(!$actor || $actor->domain == null || $target->domain !== null) {
|
||||
return;
|
||||
}
|
||||
|
@ -470,55 +470,56 @@ class Inbox
|
|||
$profile->statuses()->delete();
|
||||
$profile->delete();
|
||||
return;
|
||||
}
|
||||
$type = $this->payload['object']['type'];
|
||||
$typeCheck = in_array($type, ['Person', 'Tombstone']);
|
||||
if(!Helpers::validateUrl($actor) || !Helpers::validateUrl($obj['id']) || !$typeCheck) {
|
||||
return;
|
||||
}
|
||||
if(parse_url($obj['id'], PHP_URL_HOST) !== parse_url($actor, PHP_URL_HOST)) {
|
||||
return;
|
||||
}
|
||||
$id = $this->payload['object']['id'];
|
||||
switch ($type) {
|
||||
case 'Person':
|
||||
$profile = Profile::whereRemoteUrl($actor)->first();
|
||||
if(!$profile || $profile->private_key != null) {
|
||||
return;
|
||||
}
|
||||
Notification::whereActorId($profile->id)->delete();
|
||||
$profile->avatar()->delete();
|
||||
$profile->followers()->delete();
|
||||
$profile->following()->delete();
|
||||
$profile->likes()->delete();
|
||||
$profile->media()->delete();
|
||||
$profile->hashtags()->delete();
|
||||
$profile->statuses()->delete();
|
||||
$profile->delete();
|
||||
} else {
|
||||
$type = $this->payload['object']['type'];
|
||||
$typeCheck = in_array($type, ['Person', 'Tombstone']);
|
||||
if(!Helpers::validateUrl($actor) || !Helpers::validateUrl($obj['id']) || !$typeCheck) {
|
||||
return;
|
||||
break;
|
||||
|
||||
case 'Tombstone':
|
||||
$profile = Helpers::profileFetch($actor);
|
||||
$status = Status::whereProfileId($profile->id)
|
||||
->whereUri($id)
|
||||
->orWhere('url', $id)
|
||||
->orWhere('object_url', $id)
|
||||
->first();
|
||||
if(!$status) {
|
||||
return;
|
||||
}
|
||||
$status->directMessage()->delete();
|
||||
$status->media()->delete();
|
||||
$status->likes()->delete();
|
||||
$status->shares()->delete();
|
||||
$status->delete();
|
||||
}
|
||||
if(parse_url($obj['id'], PHP_URL_HOST) !== parse_url($actor, PHP_URL_HOST)) {
|
||||
return;
|
||||
}
|
||||
$id = $this->payload['object']['id'];
|
||||
switch ($type) {
|
||||
case 'Person':
|
||||
$profile = Profile::whereRemoteUrl($actor)->first();
|
||||
if(!$profile || $profile->private_key != null) {
|
||||
return;
|
||||
}
|
||||
Notification::whereActorId($profile->id)->delete();
|
||||
$profile->avatar()->delete();
|
||||
$profile->followers()->delete();
|
||||
$profile->following()->delete();
|
||||
$profile->likes()->delete();
|
||||
$profile->media()->delete();
|
||||
$profile->hashtags()->delete();
|
||||
$profile->statuses()->delete();
|
||||
$profile->delete();
|
||||
return;
|
||||
break;
|
||||
|
||||
default:
|
||||
return;
|
||||
break;
|
||||
break;
|
||||
|
||||
case 'Tombstone':
|
||||
$profile = Helpers::profileFetch($actor);
|
||||
$status = Status::whereProfileId($profile->id)
|
||||
->whereUri($id)
|
||||
->orWhere('url', $id)
|
||||
->orWhere('object_url', $id)
|
||||
->first();
|
||||
if(!$status) {
|
||||
return;
|
||||
}
|
||||
$status->directMessage()->delete();
|
||||
$status->media()->delete();
|
||||
$status->likes()->delete();
|
||||
$status->shares()->delete();
|
||||
$status->delete();
|
||||
return;
|
||||
break;
|
||||
|
||||
default:
|
||||
return;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -177,6 +177,7 @@ class RestrictedNames
|
|||
'help-center_',
|
||||
'help_center-',
|
||||
'i',
|
||||
'inbox',
|
||||
'img',
|
||||
'imgs',
|
||||
'image',
|
||||
|
|
|
@ -20,7 +20,7 @@ return [
|
|||
'remoteFollow' => env('AP_REMOTE_FOLLOW', false),
|
||||
|
||||
'delivery' => [
|
||||
'timeout' => env('ACTIVITYPUB_DELIVERY_TIMEOUT', 2.0),
|
||||
'timeout' => env('ACTIVITYPUB_DELIVERY_TIMEOUT', 30.0),
|
||||
'concurrency' => env('ACTIVITYPUB_DELIVERY_CONCURRENCY', 10),
|
||||
'logger' => [
|
||||
'enabled' => env('AP_LOGGER_ENABLED', false),
|
||||
|
|
|
@ -4,6 +4,7 @@ use Illuminate\Http\Request;
|
|||
|
||||
$middleware = ['auth:api','twofactor','validemail','localization', 'throttle:60,1'];
|
||||
|
||||
Route::post('/f/inbox', 'FederationController@sharedInbox');
|
||||
Route::post('/users/{username}/inbox', 'FederationController@userInbox');
|
||||
|
||||
Route::group(['prefix' => 'api'], function() use($middleware) {
|
||||
|
|
Loading…
Reference in a new issue