Add shared inbox

This commit is contained in:
Daniel Supernault 2020-11-26 00:39:01 -07:00
parent 0904a9d02f
commit 4733ca9fb9
No known key found for this signature in database
GPG key ID: 0DEF1C662C9033F7
8 changed files with 231 additions and 90 deletions

View file

@ -108,6 +108,17 @@ class FederationController extends Controller
return; 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) public function userFollowing(Request $request, $username)
{ {
abort_if(!config('federation.activitypub.enabled'), 404); abort_if(!config('federation.activitypub.enabled'), 404);

View file

@ -26,10 +26,9 @@ class InboxWorker implements ShouldQueue
* *
* @return void * @return void
*/ */
public function __construct($headers, $profile, $payload) public function __construct($headers, $payload)
{ {
$this->headers = $headers; $this->headers = $headers;
$this->profile = $profile;
$this->payload = $payload; $this->payload = $payload;
} }
@ -40,6 +39,116 @@ class InboxWorker implements ShouldQueue
*/ */
public function handle() 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);
} }
} }

View file

@ -44,6 +44,9 @@ class ProfileTransformer extends Fractal\TransformerAbstract
'mediaType' => 'image/jpeg', 'mediaType' => 'image/jpeg',
'url' => $profile->avatarUrl(), 'url' => $profile->avatarUrl(),
], ],
'endpoints' => [
'sharedInbox' => config('app.url') . '/f/inbox'
]
]; ];
} }
} }

View file

@ -135,41 +135,49 @@ class Helpers {
if(is_array($url)) { if(is_array($url)) {
$url = $url[0]; $url = $url[0];
} }
$localhosts = [
'127.0.0.1', 'localhost', '::1'
];
if(mb_substr($url, 0, 8) !== 'https://') { $hash = hash('sha256', $url);
return false; $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) { if(mb_substr($url, 0, 8) !== 'https://') {
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; return false;
} }
}
if(in_array($host, $localhosts)) { $valid = filter_var($url, FILTER_VALIDATE_URL);
return false;
}
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) 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(self::validateUrl($url) == false) {
if($url == false) {
return; return;
} }
$res = Zttp::withHeaders(self::zttpUserAgent())->get($url);
$res = json_decode($res->body(), true, 8); $hash = hash('sha256', $url);
if(json_last_error() == JSON_ERROR_NONE) { $key = "helpers:url:fetcher:sha256-{$hash}";
return $res; $ttl = now()->addMinutes(5);
} else {
return false; 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) public static function fetchProfileFromUrl($url)
@ -444,6 +458,7 @@ class Helpers {
$profile->name = isset($res['name']) ? Purify::clean($res['name']) : 'user'; $profile->name = isset($res['name']) ? Purify::clean($res['name']) : 'user';
$profile->bio = isset($res['summary']) ? Purify::clean($res['summary']) : null; $profile->bio = isset($res['summary']) ? Purify::clean($res['summary']) : null;
$profile->last_fetched_at = now(); $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(); $profile->save();
} }
} }

View file

@ -323,7 +323,7 @@ class Inbox
public function handleFollowActivity() public function handleFollowActivity()
{ {
$actor = $this->actorFirstOrCreate($this->payload['actor']); $actor = $this->actorFirstOrCreate($this->payload['actor']);
$target = $this->profile; $target = $this->actorFirstOrCreate($this->payload['object']);
if(!$actor || $actor->domain == null || $target->domain !== null) { if(!$actor || $actor->domain == null || $target->domain !== null) {
return; return;
} }
@ -470,55 +470,56 @@ class Inbox
$profile->statuses()->delete(); $profile->statuses()->delete();
$profile->delete(); $profile->delete();
return; return;
} } else {
$type = $this->payload['object']['type']; $type = $this->payload['object']['type'];
$typeCheck = in_array($type, ['Person', 'Tombstone']); $typeCheck = in_array($type, ['Person', 'Tombstone']);
if(!Helpers::validateUrl($actor) || !Helpers::validateUrl($obj['id']) || !$typeCheck) { 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();
return; return;
break; }
if(parse_url($obj['id'], PHP_URL_HOST) !== parse_url($actor, PHP_URL_HOST)) {
case 'Tombstone': return;
$profile = Helpers::profileFetch($actor); }
$status = Status::whereProfileId($profile->id) $id = $this->payload['object']['id'];
->whereUri($id) switch ($type) {
->orWhere('url', $id) case 'Person':
->orWhere('object_url', $id) $profile = Profile::whereRemoteUrl($actor)->first();
->first(); if(!$profile || $profile->private_key != null) {
if(!$status) { return;
return; }
} Notification::whereActorId($profile->id)->delete();
$status->directMessage()->delete(); $profile->avatar()->delete();
$status->media()->delete(); $profile->followers()->delete();
$status->likes()->delete(); $profile->following()->delete();
$status->shares()->delete(); $profile->likes()->delete();
$status->delete(); $profile->media()->delete();
$profile->hashtags()->delete();
$profile->statuses()->delete();
$profile->delete();
return; return;
break; break;
default: case 'Tombstone':
return; $profile = Helpers::profileFetch($actor);
break; $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;
}
} }
} }

View file

@ -177,6 +177,7 @@ class RestrictedNames
'help-center_', 'help-center_',
'help_center-', 'help_center-',
'i', 'i',
'inbox',
'img', 'img',
'imgs', 'imgs',
'image', 'image',

View file

@ -20,7 +20,7 @@ return [
'remoteFollow' => env('AP_REMOTE_FOLLOW', false), 'remoteFollow' => env('AP_REMOTE_FOLLOW', false),
'delivery' => [ 'delivery' => [
'timeout' => env('ACTIVITYPUB_DELIVERY_TIMEOUT', 2.0), 'timeout' => env('ACTIVITYPUB_DELIVERY_TIMEOUT', 30.0),
'concurrency' => env('ACTIVITYPUB_DELIVERY_CONCURRENCY', 10), 'concurrency' => env('ACTIVITYPUB_DELIVERY_CONCURRENCY', 10),
'logger' => [ 'logger' => [
'enabled' => env('AP_LOGGER_ENABLED', false), 'enabled' => env('AP_LOGGER_ENABLED', false),

View file

@ -4,6 +4,7 @@ use Illuminate\Http\Request;
$middleware = ['auth:api','twofactor','validemail','localization', 'throttle:60,1']; $middleware = ['auth:api','twofactor','validemail','localization', 'throttle:60,1'];
Route::post('/f/inbox', 'FederationController@sharedInbox');
Route::post('/users/{username}/inbox', 'FederationController@userInbox'); Route::post('/users/{username}/inbox', 'FederationController@userInbox');
Route::group(['prefix' => 'api'], function() use($middleware) { Route::group(['prefix' => 'api'], function() use($middleware) {