Merge pull request #2454 from pixelfed/staging

Add shared inbox
This commit is contained in:
daniel 2020-11-26 19:50:37 -07:00 committed by GitHub
commit 72cff7ed12
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
12 changed files with 244 additions and 96 deletions

View file

@ -17,6 +17,7 @@
- Add MediaBlocklist feature ([ba1f7e7e](https://github.com/pixelfed/pixelfed/commit/ba1f7e7e))
- New Discover Layout, add trending hashtags, places and posts ([c251d41b](https://github.com/pixelfed/pixelfed/commit/c251d41b))
- Add Password change email notification ([de1cca4f](https://github.com/pixelfed/pixelfed/commit/de1cca4f))
- Add shared inbox ([4733ca9f](https://github.com/pixelfed/pixelfed/commit/4733ca9f))
### Updated
- Updated PostComponent, fix remote urls ([42716ccc](https://github.com/pixelfed/pixelfed/commit/42716ccc))
@ -101,6 +102,7 @@
- Updated EmailService, make case insensitive. ([1b41d664](https://github.com/pixelfed/pixelfed/commit/1b41d664))
- Updated DiscoverController, fix trending api. ([2ab2c9a](https://github.com/pixelfed/pixelfed/commit/2ab2c9a))
- Updated Dark Mode layout. ([d6f8170](https://github.com/pixelfed/pixelfed/commit/d6f8170))
- Updated federation config, make sharedInbox enabled by default. ([6e3522c0](https://github.com/pixelfed/pixelfed/commit/6e3522c0))
## [v0.10.9 (2020-04-17)](https://github.com/pixelfed/pixelfed/compare/v0.10.8...v0.10.9)
### Added

View file

@ -90,7 +90,7 @@ class RegisterController extends Controller
}
$restricted = RestrictedNames::get();
if (in_array($value, $restricted)) {
if (in_array(strtolower($value), array_map('strtolower', $restricted))) {
return $fail('Username cannot be used.');
}
},

View file

@ -704,7 +704,7 @@ class DirectMessageController extends Controller
public function remoteDeliver($dm)
{
$profile = $dm->author;
$url = $dm->recipient->inbox_url;
$url = $dm->recipient->sharedInbox ?? $dm->recipient->inbox_url;
$tags = [
[
@ -760,7 +760,7 @@ class DirectMessageController extends Controller
public function remoteDelete($dm)
{
$profile = $dm->author;
$url = $dm->recipient->inbox_url;
$url = $dm->recipient->sharedInbox ?? $dm->recipient->inbox_url;
$body = [
'@context' => [

View file

@ -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);

View file

@ -9,10 +9,10 @@ use App\Util\ActivityPub\{
Inbox
};
use Illuminate\Bus\Queueable;
use Illuminate\Queue\SerializesModels;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
use Zttp\Zttp;
class InboxValidator implements ShouldQueue

View file

@ -3,12 +3,17 @@
namespace App\Jobs\InboxPipeline;
use App\Profile;
use App\Util\ActivityPub\Inbox;
use App\Util\ActivityPub\{
Helpers,
HttpSignature,
Inbox
};
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
use Zttp\Zttp;
class InboxWorker implements ShouldQueue
{
@ -26,10 +31,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 +44,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);
}
}

View file

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

View file

@ -136,6 +136,11 @@ class Helpers {
$url = $url[0];
}
$hash = hash('sha256', $url);
$key = "helpers:url:valid:sha256-{$hash}";
$ttl = now()->addMinutes(5);
$valid = Cache::remember($key, $ttl, function() use($url) {
$localhosts = [
'127.0.0.1', 'localhost', '::1'
];
@ -169,6 +174,9 @@ class Helpers {
return false;
}
return $url;
});
return $valid;
}
@ -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);
$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();
}
}

View file

@ -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,7 +470,7 @@ class Inbox
$profile->statuses()->delete();
$profile->delete();
return;
}
} else {
$type = $this->payload['object']['type'];
$typeCheck = in_array($type, ['Person', 'Tombstone']);
if(!Helpers::validateUrl($actor) || !Helpers::validateUrl($obj['id']) || !$typeCheck) {
@ -521,6 +521,7 @@ class Inbox
break;
}
}
}
public function handleLikeActivity()
{

View file

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

View file

@ -15,12 +15,12 @@ return [
'enabled' => env('ACTIVITY_PUB', false),
'outbox' => env('AP_OUTBOX', true),
'inbox' => env('AP_INBOX', true),
'sharedInbox' => env('AP_SHAREDINBOX', false),
'sharedInbox' => env('AP_SHAREDINBOX', true),
'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),

View file

@ -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) {