mirror of
https://github.com/pixelfed/pixelfed.git
synced 2024-11-22 22:41:27 +00:00
commit
72cff7ed12
12 changed files with 244 additions and 96 deletions
|
@ -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
|
||||
|
|
|
@ -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.');
|
||||
}
|
||||
},
|
||||
|
|
|
@ -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' => [
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -44,6 +44,9 @@ class ProfileTransformer extends Fractal\TransformerAbstract
|
|||
'mediaType' => 'image/jpeg',
|
||||
'url' => $profile->avatarUrl(),
|
||||
],
|
||||
'endpoints' => [
|
||||
'sharedInbox' => config('app.url') . '/f/inbox'
|
||||
]
|
||||
];
|
||||
}
|
||||
}
|
||||
|
|
|
@ -135,39 +135,47 @@ 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);
|
||||
|
||||
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 $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);
|
||||
$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',
|
||||
|
|
|
@ -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),
|
||||
|
|
|
@ -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