From 240e6bbe4f57b320bb00a3b30f0a1a907c8975d7 Mon Sep 17 00:00:00 2001 From: Daniel Supernault Date: Wed, 7 Feb 2024 02:47:34 -0700 Subject: [PATCH 1/8] Update NodeinfoService, disable redirects --- app/Services/NodeinfoService.php | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/app/Services/NodeinfoService.php b/app/Services/NodeinfoService.php index 10575ff9f..6284538f0 100644 --- a/app/Services/NodeinfoService.php +++ b/app/Services/NodeinfoService.php @@ -22,7 +22,10 @@ class NodeinfoService $wk = $url . '/.well-known/nodeinfo'; try { - $res = Http::withHeaders($headers) + $res = Http::withOptions([ + 'allow_redirects' => false, + ]) + ->withHeaders($headers) ->timeout(5) ->get($wk); } catch (RequestException $e) { @@ -61,7 +64,10 @@ class NodeinfoService } try { - $res = Http::withHeaders($headers) + $res = Http::withOptions([ + 'allow_redirects' => false, + ]) + ->withHeaders($headers) ->timeout(5) ->get($href); } catch (RequestException $e) { From 289cad470b0a04c2836e19aa57d6566df95ce668 Mon Sep 17 00:00:00 2001 From: Daniel Supernault Date: Wed, 7 Feb 2024 02:49:29 -0700 Subject: [PATCH 2/8] Update Instance model, add entity casts --- app/Instance.php | 120 ++++++++++++++++++++++++++--------------------- 1 file changed, 67 insertions(+), 53 deletions(-) diff --git a/app/Instance.php b/app/Instance.php index 6a7b8e6f2..77752d498 100644 --- a/app/Instance.php +++ b/app/Instance.php @@ -6,63 +6,77 @@ use Illuminate\Database\Eloquent\Model; class Instance extends Model { - protected $fillable = ['domain', 'banned', 'auto_cw', 'unlisted', 'notes']; + protected $casts = [ + 'last_crawled_at' => 'datetime', + 'actors_last_synced_at' => 'datetime', + 'notes' => 'array', + 'nodeinfo_last_fetched' => 'datetime', + 'delivery_next_after' => 'datetime', + ]; - public function profiles() - { - return $this->hasMany(Profile::class, 'domain', 'domain'); - } + protected $fillable = [ + 'domain', + 'banned', + 'auto_cw', + 'unlisted', + 'notes' + ]; - public function statuses() - { - return $this->hasManyThrough( - Status::class, - Profile::class, - 'domain', - 'profile_id', - 'domain', - 'id' - ); - } + public function profiles() + { + return $this->hasMany(Profile::class, 'domain', 'domain'); + } - public function reported() - { - return $this->hasManyThrough( - Report::class, - Profile::class, - 'domain', - 'reported_profile_id', - 'domain', - 'id' - ); - } + public function statuses() + { + return $this->hasManyThrough( + Status::class, + Profile::class, + 'domain', + 'profile_id', + 'domain', + 'id' + ); + } - public function reports() - { - return $this->hasManyThrough( - Report::class, - Profile::class, - 'domain', - 'profile_id', - 'domain', - 'id' - ); - } + public function reported() + { + return $this->hasManyThrough( + Report::class, + Profile::class, + 'domain', + 'reported_profile_id', + 'domain', + 'id' + ); + } - public function media() - { - return $this->hasManyThrough( - Media::class, - Profile::class, - 'domain', - 'profile_id', - 'domain', - 'id' - ); - } + public function reports() + { + return $this->hasManyThrough( + Report::class, + Profile::class, + 'domain', + 'profile_id', + 'domain', + 'id' + ); + } - public function getUrl() - { - return url("/i/admin/instances/show/{$this->id}"); - } + public function media() + { + return $this->hasManyThrough( + Media::class, + Profile::class, + 'domain', + 'profile_id', + 'domain', + 'id' + ); + } + + public function getUrl() + { + return url("/i/admin/instances/show/{$this->id}"); + } } From ac01f51ab66df0b4f4657e70bbe0befe2c022b9f Mon Sep 17 00:00:00 2001 From: Daniel Supernault Date: Wed, 7 Feb 2024 02:51:50 -0700 Subject: [PATCH 3/8] Update FetchNodeinfoPipeline, use more efficient dispatch --- .../FetchNodeinfoPipeline.php | 97 ++++++++++++------- 1 file changed, 62 insertions(+), 35 deletions(-) diff --git a/app/Jobs/InstancePipeline/FetchNodeinfoPipeline.php b/app/Jobs/InstancePipeline/FetchNodeinfoPipeline.php index b8c79d67f..943281bb4 100644 --- a/app/Jobs/InstancePipeline/FetchNodeinfoPipeline.php +++ b/app/Jobs/InstancePipeline/FetchNodeinfoPipeline.php @@ -4,6 +4,7 @@ namespace App\Jobs\InstancePipeline; use Illuminate\Bus\Queueable; use Illuminate\Contracts\Queue\ShouldBeUnique; +use Illuminate\Contracts\Queue\ShouldBeUniqueUntilProcessing; use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Foundation\Bus\Dispatchable; use Illuminate\Queue\InteractsWithQueue; @@ -12,45 +13,71 @@ use Illuminate\Support\Facades\Http; use App\Instance; use App\Profile; use App\Services\NodeinfoService; +use Illuminate\Contracts\Cache\Repository; +use Illuminate\Support\Facades\Cache; -class FetchNodeinfoPipeline implements ShouldQueue +class FetchNodeinfoPipeline implements ShouldQueue, ShouldBeUniqueUntilProcessing { - use Dispatchable, InteractsWithQueue, Queueable, SerializesModels; + use Dispatchable, InteractsWithQueue, Queueable, SerializesModels; - protected $instance; + protected $instance; - /** - * Create a new job instance. - * - * @return void - */ - public function __construct(Instance $instance) - { - $this->instance = $instance; - } + /** + * Create a new job instance. + * + * @return void + */ + public function __construct(Instance $instance) + { + $this->instance = $instance; + } - /** - * Execute the job. - * - * @return void - */ - public function handle() - { - $instance = $this->instance; + /** + * The number of seconds after which the job's unique lock will be released. + * + * @var int + */ + public $uniqueFor = 14400; - $ni = NodeinfoService::get($instance->domain); - if($ni) { - if(isset($ni['software']) && is_array($ni['software']) && isset($ni['software']['name'])) { - $software = $ni['software']['name']; - $instance->software = strtolower(strip_tags($software)); - $instance->last_crawled_at = now(); - $instance->user_count = Profile::whereDomain($instance->domain)->count(); - $instance->save(); - } - } else { - $instance->user_count = Profile::whereDomain($instance->domain)->count(); - $instance->last_crawled_at = now(); - $instance->save(); - } - } + /** + * Get the unique ID for the job. + */ + public function uniqueId(): string + { + return $this->instance->id; + } + + /** + * Execute the job. + * + * @return void + */ + public function handle() + { + $instance = $this->instance; + + if( $instance->nodeinfo_last_fetched && + $instance->nodeinfo_last_fetched->gt(now()->subHours(12)) || + $instance->delivery_timeout && + $instance->delivery_next_after->gt(now()) + ) { + return; + } + + $ni = NodeinfoService::get($instance->domain); + $instance->last_crawled_at = now(); + if($ni) { + if(isset($ni['software']) && is_array($ni['software']) && isset($ni['software']['name'])) { + $software = $ni['software']['name']; + $instance->software = strtolower(strip_tags($software)); + $instance->user_count = Profile::whereDomain($instance->domain)->count(); + $instance->nodeinfo_last_fetched = now(); + $instance->save(); + } + } else { + $instance->delivery_timeout = 1; + $instance->delivery_next_after = now()->addHours(14); + $instance->save(); + } + } } From 1e3acadefb200fbb4d594909083c5a275e413b77 Mon Sep 17 00:00:00 2001 From: Daniel Supernault Date: Wed, 7 Feb 2024 02:52:37 -0700 Subject: [PATCH 4/8] Update horizon.php config --- config/horizon.php | 349 +++++++++++++++++++++++---------------------- 1 file changed, 175 insertions(+), 174 deletions(-) diff --git a/config/horizon.php b/config/horizon.php index f9cfd960e..5aa37f2fe 100644 --- a/config/horizon.php +++ b/config/horizon.php @@ -2,201 +2,202 @@ return [ - /* - |-------------------------------------------------------------------------- - | Horizon Domain - |-------------------------------------------------------------------------- - | - | This is the subdomain where Horizon will be accessible from. If this - | setting is null, Horizon will reside under the same domain as the - | application. Otherwise, this value will serve as the subdomain. - | - */ + /* + |-------------------------------------------------------------------------- + | Horizon Domain + |-------------------------------------------------------------------------- + | + | This is the subdomain where Horizon will be accessible from. If this + | setting is null, Horizon will reside under the same domain as the + | application. Otherwise, this value will serve as the subdomain. + | + */ - 'domain' => null, + 'domain' => null, - /* - |-------------------------------------------------------------------------- - | Horizon Path - |-------------------------------------------------------------------------- - | - | This is the URI path where Horizon will be accessible from. Feel free - | to change this path to anything you like. Note that the URI will not - | affect the paths of its internal API that aren't exposed to users. - | - */ + /* + |-------------------------------------------------------------------------- + | Horizon Path + |-------------------------------------------------------------------------- + | + | This is the URI path where Horizon will be accessible from. Feel free + | to change this path to anything you like. Note that the URI will not + | affect the paths of its internal API that aren't exposed to users. + | + */ - 'path' => 'horizon', + 'path' => 'horizon', - /* - |-------------------------------------------------------------------------- - | Horizon Redis Connection - |-------------------------------------------------------------------------- - | - | This is the name of the Redis connection where Horizon will store the - | meta information required for it to function. It includes the list - | of supervisors, failed jobs, job metrics, and other information. - | - */ + /* + |-------------------------------------------------------------------------- + | Horizon Redis Connection + |-------------------------------------------------------------------------- + | + | This is the name of the Redis connection where Horizon will store the + | meta information required for it to function. It includes the list + | of supervisors, failed jobs, job metrics, and other information. + | + */ - 'use' => 'default', + 'use' => 'default', - /* - |-------------------------------------------------------------------------- - | Horizon Redis Prefix - |-------------------------------------------------------------------------- - | - | This prefix will be used when storing all Horizon data in Redis. You - | may modify the prefix when you are running multiple installations - | of Horizon on the same server so that they don't have problems. - | - */ + /* + |-------------------------------------------------------------------------- + | Horizon Redis Prefix + |-------------------------------------------------------------------------- + | + | This prefix will be used when storing all Horizon data in Redis. You + | may modify the prefix when you are running multiple installations + | of Horizon on the same server so that they don't have problems. + | + */ - 'prefix' => env('HORIZON_PREFIX', 'horizon-'), + 'prefix' => env('HORIZON_PREFIX', 'horizon-'), - /* - |-------------------------------------------------------------------------- - | Horizon Route Middleware - |-------------------------------------------------------------------------- - | - | These middleware will get attached onto each Horizon route, giving you - | the chance to add your own middleware to this list or change any of - | the existing middleware. Or, you can simply stick with this list. - | - */ + /* + |-------------------------------------------------------------------------- + | Horizon Route Middleware + |-------------------------------------------------------------------------- + | + | These middleware will get attached onto each Horizon route, giving you + | the chance to add your own middleware to this list or change any of + | the existing middleware. Or, you can simply stick with this list. + | + */ - 'middleware' => ['web'], + 'middleware' => ['web'], - /* - |-------------------------------------------------------------------------- - | Queue Wait Time Thresholds - |-------------------------------------------------------------------------- - | - | This option allows you to configure when the LongWaitDetected event - | will be fired. Every connection / queue combination may have its - | own, unique threshold (in seconds) before this event is fired. - | - */ + /* + |-------------------------------------------------------------------------- + | Queue Wait Time Thresholds + |-------------------------------------------------------------------------- + | + | This option allows you to configure when the LongWaitDetected event + | will be fired. Every connection / queue combination may have its + | own, unique threshold (in seconds) before this event is fired. + | + */ - 'waits' => [ - 'redis:feed' => 30, - 'redis:follow' => 30, - 'redis:shared' => 30, - 'redis:default' => 30, - 'redis:inbox' => 30, - 'redis:low' => 30, - 'redis:high' => 30, - 'redis:delete' => 30, - 'redis:story' => 30, - 'redis:mmo' => 30, - ], + 'waits' => [ + 'redis:feed' => 30, + 'redis:follow' => 30, + 'redis:shared' => 30, + 'redis:default' => 30, + 'redis:inbox' => 30, + 'redis:low' => 30, + 'redis:high' => 30, + 'redis:delete' => 30, + 'redis:story' => 30, + 'redis:mmo' => 30, + 'redis:intbg' => 30, + ], - /* - |-------------------------------------------------------------------------- - | Job Trimming Times - |-------------------------------------------------------------------------- - | - | Here you can configure for how long (in minutes) you desire Horizon to - | persist the recent and failed jobs. Typically, recent jobs are kept - | for one hour while all failed jobs are stored for an entire week. - | - */ + /* + |-------------------------------------------------------------------------- + | Job Trimming Times + |-------------------------------------------------------------------------- + | + | Here you can configure for how long (in minutes) you desire Horizon to + | persist the recent and failed jobs. Typically, recent jobs are kept + | for one hour while all failed jobs are stored for an entire week. + | + */ - 'trim' => [ - 'recent' => 60, - 'pending' => 60, - 'completed' => 60, - 'recent_failed' => 10080, - 'failed' => 10080, - 'monitored' => 10080, - ], + 'trim' => [ + 'recent' => 60, + 'pending' => 60, + 'completed' => 60, + 'recent_failed' => 10080, + 'failed' => 10080, + 'monitored' => 10080, + ], - /* - |-------------------------------------------------------------------------- - | Metrics - |-------------------------------------------------------------------------- - | - | Here you can configure how many snapshots should be kept to display in - | the metrics graph. This will get used in combination with Horizon's - | `horizon:snapshot` schedule to define how long to retain metrics. - | - */ + /* + |-------------------------------------------------------------------------- + | Metrics + |-------------------------------------------------------------------------- + | + | Here you can configure how many snapshots should be kept to display in + | the metrics graph. This will get used in combination with Horizon's + | `horizon:snapshot` schedule to define how long to retain metrics. + | + */ - 'metrics' => [ - 'trim_snapshots' => [ - 'job' => 24, - 'queue' => 24, - ], - ], + 'metrics' => [ + 'trim_snapshots' => [ + 'job' => 24, + 'queue' => 24, + ], + ], - /* - |-------------------------------------------------------------------------- - | Fast Termination - |-------------------------------------------------------------------------- - | - | When this option is enabled, Horizon's "terminate" command will not - | wait on all of the workers to terminate unless the --wait option - | is provided. Fast termination can shorten deployment delay by - | allowing a new instance of Horizon to start while the last - | instance will continue to terminate each of its workers. - | - */ + /* + |-------------------------------------------------------------------------- + | Fast Termination + |-------------------------------------------------------------------------- + | + | When this option is enabled, Horizon's "terminate" command will not + | wait on all of the workers to terminate unless the --wait option + | is provided. Fast termination can shorten deployment delay by + | allowing a new instance of Horizon to start while the last + | instance will continue to terminate each of its workers. + | + */ - 'fast_termination' => false, + 'fast_termination' => false, - /* - |-------------------------------------------------------------------------- - | Memory Limit (MB) - |-------------------------------------------------------------------------- - | - | This value describes the maximum amount of memory the Horizon worker - | may consume before it is terminated and restarted. You should set - | this value according to the resources available to your server. - | - */ + /* + |-------------------------------------------------------------------------- + | Memory Limit (MB) + |-------------------------------------------------------------------------- + | + | This value describes the maximum amount of memory the Horizon worker + | may consume before it is terminated and restarted. You should set + | this value according to the resources available to your server. + | + */ - 'memory_limit' => env('HORIZON_MEMORY_LIMIT', 64), + 'memory_limit' => env('HORIZON_MEMORY_LIMIT', 64), - /* - |-------------------------------------------------------------------------- - | Queue Worker Configuration - |-------------------------------------------------------------------------- - | - | Here you may define the queue worker settings used by your application - | in all environments. These supervisors and settings handle all your - | queued jobs and will be provisioned by Horizon during deployment. - | - */ + /* + |-------------------------------------------------------------------------- + | Queue Worker Configuration + |-------------------------------------------------------------------------- + | + | Here you may define the queue worker settings used by your application + | in all environments. These supervisors and settings handle all your + | queued jobs and will be provisioned by Horizon during deployment. + | + */ - 'environments' => [ - 'production' => [ - 'supervisor-1' => [ - 'connection' => 'redis', - 'queue' => ['high', 'default', 'follow', 'shared', 'inbox', 'feed', 'low', 'story', 'delete', 'mmo'], - 'balance' => env('HORIZON_BALANCE_STRATEGY', 'auto'), - 'minProcesses' => env('HORIZON_MIN_PROCESSES', 1), - 'maxProcesses' => env('HORIZON_MAX_PROCESSES', 20), - 'memory' => env('HORIZON_SUPERVISOR_MEMORY', 64), - 'tries' => env('HORIZON_SUPERVISOR_TRIES', 3), - 'nice' => env('HORIZON_SUPERVISOR_NICE', 0), - 'timeout' => env('HORIZON_SUPERVISOR_TIMEOUT', 300), - ], - ], + 'environments' => [ + 'production' => [ + 'supervisor-1' => [ + 'connection' => 'redis', + 'queue' => ['high', 'default', 'follow', 'shared', 'inbox', 'feed', 'low', 'story', 'delete', 'mmo', 'intbg'], + 'balance' => env('HORIZON_BALANCE_STRATEGY', 'auto'), + 'minProcesses' => env('HORIZON_MIN_PROCESSES', 1), + 'maxProcesses' => env('HORIZON_MAX_PROCESSES', 20), + 'memory' => env('HORIZON_SUPERVISOR_MEMORY', 64), + 'tries' => env('HORIZON_SUPERVISOR_TRIES', 3), + 'nice' => env('HORIZON_SUPERVISOR_NICE', 0), + 'timeout' => env('HORIZON_SUPERVISOR_TIMEOUT', 300), + ], + ], - 'local' => [ - 'supervisor-1' => [ - 'connection' => 'redis', - 'queue' => ['high', 'default', 'follow', 'shared', 'inbox', 'feed', 'low', 'story', 'delete', 'mmo'], - 'balance' => 'auto', - 'minProcesses' => 1, - 'maxProcesses' => 20, - 'memory' => 128, - 'tries' => 3, - 'nice' => 0, - 'timeout' => 300 - ], - ], - ], + 'local' => [ + 'supervisor-1' => [ + 'connection' => 'redis', + 'queue' => ['high', 'default', 'follow', 'shared', 'inbox', 'feed', 'low', 'story', 'delete', 'mmo', 'intbg'], + 'balance' => 'auto', + 'minProcesses' => 1, + 'maxProcesses' => 20, + 'memory' => 128, + 'tries' => 3, + 'nice' => 0, + 'timeout' => 300 + ], + ], + ], - 'darkmode' => env('HORIZON_DARKMODE', false), + 'darkmode' => env('HORIZON_DARKMODE', false), ]; From 01b33fb37efdb595709b5379ba5482b72cf4093f Mon Sep 17 00:00:00 2001 From: Daniel Supernault Date: Wed, 7 Feb 2024 03:43:20 -0700 Subject: [PATCH 5/8] Update PublicApiController, consume InstanceService blocked domains for account and statuses endpoints --- app/Http/Controllers/PublicApiController.php | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/app/Http/Controllers/PublicApiController.php b/app/Http/Controllers/PublicApiController.php index f888eb512..78008eda4 100644 --- a/app/Http/Controllers/PublicApiController.php +++ b/app/Http/Controllers/PublicApiController.php @@ -42,6 +42,7 @@ use App\Services\{ use App\Jobs\StatusPipeline\NewStatusPipeline; use League\Fractal\Serializer\ArraySerializer; use League\Fractal\Pagination\IlluminatePaginatorAdapter; +use App\Services\InstanceService; class PublicApiController extends Controller { @@ -661,6 +662,10 @@ class PublicApiController extends Controller public function account(Request $request, $id) { $res = AccountService::get($id); + if($res && isset($res['local'], $res['url']) && !$res['local']) { + $domain = parse_url($res['url'], PHP_URL_HOST); + abort_if(in_array($domain, InstanceService::getBannedDomains()), 404); + } return response()->json($res); } @@ -680,6 +685,11 @@ class PublicApiController extends Controller $profile = AccountService::get($id); abort_if(!$profile, 404); + if($profile && isset($profile['local'], $profile['url']) && !$profile['local']) { + $domain = parse_url($profile['url'], PHP_URL_HOST); + abort_if(in_array($domain, InstanceService::getBannedDomains()), 404); + } + $limit = $request->limit ?? 9; $max_id = $request->max_id; $min_id = $request->min_id; From 5b284cacea474367701041093c445cff545ad742 Mon Sep 17 00:00:00 2001 From: Daniel Supernault Date: Wed, 7 Feb 2024 04:41:12 -0700 Subject: [PATCH 6/8] Update ApiV1Controller, enforce blocked instance domain logic --- app/Http/Controllers/Api/ApiV1Controller.php | 69 ++++++++++++++++++-- 1 file changed, 65 insertions(+), 4 deletions(-) diff --git a/app/Http/Controllers/Api/ApiV1Controller.php b/app/Http/Controllers/Api/ApiV1Controller.php index dd0cbd062..b94a11c92 100644 --- a/app/Http/Controllers/Api/ApiV1Controller.php +++ b/app/Http/Controllers/Api/ApiV1Controller.php @@ -219,6 +219,10 @@ class ApiV1Controller extends Controller if(!$res) { return response()->json(['error' => 'Record not found'], 404); } + if($res && strpos($res['acct'], '@') != -1) { + $domain = parse_url($res['url'], PHP_URL_HOST); + abort_if(in_array($domain, InstanceService::getBannedDomains()), 404); + } return $this->json($res); } @@ -483,6 +487,11 @@ class ApiV1Controller extends Controller $limit = $request->input('limit', 10); $napi = $request->has(self::PF_API_ENTITY_KEY); + if($account && strpos($account['acct'], '@') != -1) { + $domain = parse_url($account['url'], PHP_URL_HOST); + abort_if(in_array($domain, InstanceService::getBannedDomains()), 404); + } + if(intval($pid) !== intval($account['id'])) { if($account['locked']) { if(!FollowerService::follows($pid, $account['id'])) { @@ -575,6 +584,11 @@ class ApiV1Controller extends Controller $limit = $request->input('limit', 10); $napi = $request->has(self::PF_API_ENTITY_KEY); + if($account && strpos($account['acct'], '@') != -1) { + $domain = parse_url($account['url'], PHP_URL_HOST); + abort_if(in_array($domain, InstanceService::getBannedDomains()), 404); + } + if(intval($pid) !== intval($account['id'])) { if($account['locked']) { if(!FollowerService::follows($pid, $account['id'])) { @@ -676,6 +690,11 @@ class ApiV1Controller extends Controller return $this->json(['error' => 'Account not found'], 404); } + if($profile && strpos($profile['acct'], '@') != -1) { + $domain = parse_url($profile['url'], PHP_URL_HOST); + abort_if(in_array($domain, InstanceService::getBannedDomains()), 404); + } + $limit = $request->limit ?? 20; $max_id = $request->max_id; $min_id = $request->min_id; @@ -766,6 +785,11 @@ class ApiV1Controller extends Controller ->whereNull('status') ->findOrFail($id); + if($target && $target->domain) { + $domain = $target->domain; + abort_if(in_array($domain, InstanceService::getBannedDomains()), 404); + } + $private = (bool) $target->is_private; $remote = (bool) $target->domain; $blocked = UserFilter::whereUserId($target->id) @@ -1252,14 +1276,19 @@ class ApiV1Controller extends Controller $user = $request->user(); abort_if($user->has_roles && !UserRoleService::can('can-like', $user->id), 403, 'Invalid permissions for this action'); - AccountService::setLastActive($user->id); - $status = StatusService::getMastodon($id, false); - abort_unless($status, 400); + abort_unless($status, 404); + + if($status && isset($status['account'], $status['account']['acct']) && strpos($status['account']['acct'], '@') != -1) { + $domain = parse_url($status['account']['url'], PHP_URL_HOST); + abort_if(in_array($domain, InstanceService::getBannedDomains()), 404); + } $spid = $status['account']['id']; + AccountService::setLastActive($user->id); + if(intval($spid) !== intval($user->profile_id)) { if($status['visibility'] == 'private') { abort_if(!FollowerService::follows($user->profile_id, $spid), 403); @@ -1404,6 +1433,11 @@ class ApiV1Controller extends Controller return response()->json(['error' => 'Record not found'], 404); } + if($target && strpos($target['acct'], '@') != -1) { + $domain = parse_url($target['url'], PHP_URL_HOST); + abort_if(in_array($domain, InstanceService::getBannedDomains()), 404); + } + $followRequest = FollowRequest::whereFollowingId($pid)->whereFollowerId($id)->first(); if(!$followRequest) { @@ -2011,6 +2045,11 @@ class ApiV1Controller extends Controller $account = Profile::findOrFail($id); + if($account && $account->domain) { + $domain = $account->domain; + abort_if(in_array($domain, InstanceService::getBannedDomains()), 404); + } + $count = UserFilterService::muteCount($pid); $maxLimit = intval(config('instance.user_filters.max_user_mutes')); if($count == 0) { @@ -2653,6 +2692,11 @@ class ApiV1Controller extends Controller abort(404); } + if($res && isset($res['account'], $res['account']['acct'], $res['account']['url']) && strpos($res['account']['acct'], '@') != -1) { + $domain = parse_url($res['account']['url'], PHP_URL_HOST); + abort_if(in_array($domain, InstanceService::getBannedDomains()), 404); + } + $scope = $res['visibility']; if(!in_array($scope, ['public', 'unlisted'])) { if($scope === 'private') { @@ -2697,6 +2741,11 @@ class ApiV1Controller extends Controller return response('', 404); } + if($status && isset($status['account'], $status['account']['acct']) && strpos($status['account']['acct'], '@') != -1) { + $domain = parse_url($status['account']['url'], PHP_URL_HOST); + abort_if(in_array($domain, InstanceService::getBannedDomains()), 404); + } + if(intval($status['account']['id']) !== intval($user->profile_id)) { if($status['visibility'] == 'private') { if(!FollowerService::follows($user->profile_id, $status['account']['id'])) { @@ -2780,6 +2829,10 @@ class ApiV1Controller extends Controller $status = Status::findOrFail($id); $account = AccountService::get($status->profile_id, true); abort_if(!$account, 404); + if($account && strpos($account['acct'], '@') != -1) { + $domain = parse_url($account['url'], PHP_URL_HOST); + abort_if(in_array($domain, InstanceService::getBannedDomains()), 404); + } $author = intval($status->profile_id) === intval($pid) || $user->is_admin; $napi = $request->has(self::PF_API_ENTITY_KEY); @@ -2871,6 +2924,10 @@ class ApiV1Controller extends Controller $pid = $user->profile_id; $status = Status::findOrFail($id); $account = AccountService::get($status->profile_id, true); + if($account && strpos($account['acct'], '@') != -1) { + $domain = parse_url($account['url'], PHP_URL_HOST); + abort_if(in_array($domain, InstanceService::getBannedDomains()), 404); + } abort_if(!$account, 404); $author = intval($status->profile_id) === intval($pid) || $user->is_admin; $napi = $request->has(self::PF_API_ENTITY_KEY); @@ -3200,7 +3257,11 @@ class ApiV1Controller extends Controller abort_if($user->has_roles && !UserRoleService::can('can-share', $user->id), 403, 'Invalid permissions for this action'); AccountService::setLastActive($user->id); $status = Status::whereScope('public')->findOrFail($id); - + if($status && ($status->uri || $status->url || $status->object_url)) { + $url = $status->uri ?? $status->url ?? $status->object_url; + $domain = parse_url($url, PHP_URL_HOST); + abort_if(in_array($domain, InstanceService::getBannedDomains()), 404); + } if(intval($status->profile_id) !== intval($user->profile_id)) { if($status->scope == 'private') { abort_if(!FollowerService::follows($user->profile_id, $status->profile_id), 403); From 6921d3568e7662b55268a0b2ab5799758735b74a Mon Sep 17 00:00:00 2001 From: Daniel Supernault Date: Wed, 7 Feb 2024 04:42:27 -0700 Subject: [PATCH 7/8] Add InstanceMananger command --- app/Console/Commands/InstanceManager.php | 298 +++++++++++++++++++++++ 1 file changed, 298 insertions(+) create mode 100644 app/Console/Commands/InstanceManager.php diff --git a/app/Console/Commands/InstanceManager.php b/app/Console/Commands/InstanceManager.php new file mode 100644 index 000000000..a495d9617 --- /dev/null +++ b/app/Console/Commands/InstanceManager.php @@ -0,0 +1,298 @@ +recalculateStats(); + break; + + case 'Unlisted Instances': + return $this->viewUnlistedInstances(); + break; + + case 'Banned Instances': + return $this->viewBannedInstances(); + break; + + case 'Unlist Instance': + return $this->unlistInstance(); + break; + + case 'Ban Instance': + return $this->banInstance(); + break; + + case 'Unban Instance': + return $this->unbanInstance(); + break; + + case 'Relist Instance': + return $this->relistInstance(); + break; + } + } + + protected function recalculateStats() + { + $instanceCount = Instance::count(); + $confirmed = confirm('Do you want to recalculate stats for all ' . $instanceCount . ' instances?'); + if(!$confirmed) { + $this->error('Aborting...'); + exit; + } + + $users = progress( + label: 'Updating instance stats...', + steps: Instance::all(), + callback: fn ($instance) => $this->updateInstanceStats($instance), + ); + } + + protected function updateInstanceStats($instance) + { + FetchNodeinfoPipeline::dispatch($instance)->onQueue('intbg'); + } + + protected function unlistInstance() + { + $id = search( + 'Search by domain', + fn (string $value) => strlen($value) > 0 + ? Instance::whereUnlisted(false)->where('domain', 'like', "%{$value}%")->pluck('domain', 'id')->all() + : [] + ); + + $instance = Instance::find($id); + if(!$instance) { + $this->error('Oops, an error occured'); + exit; + } + + $tbl = [ + [ + $instance->domain, + number_format($instance->status_count), + number_format($instance->user_count), + ] + ]; + table( + ['Domain', 'Status Count', 'User Count'], + $tbl + ); + + $confirmed = confirm('Are you sure you want to unlist this instance?'); + if(!$confirmed) { + $this->error('Aborting instance unlisting'); + exit; + } + + $instance->unlisted = true; + $instance->save(); + InstanceService::refresh(); + $this->info('Successfully unlisted ' . $instance->domain . '!'); + exit; + } + + protected function relistInstance() + { + $id = search( + 'Search by domain', + fn (string $value) => strlen($value) > 0 + ? Instance::whereUnlisted(true)->where('domain', 'like', "%{$value}%")->pluck('domain', 'id')->all() + : [] + ); + + $instance = Instance::find($id); + if(!$instance) { + $this->error('Oops, an error occured'); + exit; + } + + $tbl = [ + [ + $instance->domain, + number_format($instance->status_count), + number_format($instance->user_count), + ] + ]; + table( + ['Domain', 'Status Count', 'User Count'], + $tbl + ); + + $confirmed = confirm('Are you sure you want to re-list this instance?'); + if(!$confirmed) { + $this->error('Aborting instance re-listing'); + exit; + } + + $instance->unlisted = false; + $instance->save(); + InstanceService::refresh(); + $this->info('Successfully re-listed ' . $instance->domain . '!'); + exit; + } + + protected function banInstance() + { + $id = search( + 'Search by domain', + fn (string $value) => strlen($value) > 0 + ? Instance::whereBanned(false)->where('domain', 'like', "%{$value}%")->pluck('domain', 'id')->all() + : [] + ); + + $instance = Instance::find($id); + if(!$instance) { + $this->error('Oops, an error occured'); + exit; + } + + $tbl = [ + [ + $instance->domain, + number_format($instance->status_count), + number_format($instance->user_count), + ] + ]; + table( + ['Domain', 'Status Count', 'User Count'], + $tbl + ); + + $confirmed = confirm('Are you sure you want to ban this instance?'); + if(!$confirmed) { + $this->error('Aborting instance ban'); + exit; + } + + $instance->banned = true; + $instance->save(); + InstanceService::refresh(); + $this->info('Successfully banned ' . $instance->domain . '!'); + exit; + } + + protected function unbanInstance() + { + $id = search( + 'Search by domain', + fn (string $value) => strlen($value) > 0 + ? Instance::whereBanned(true)->where('domain', 'like', "%{$value}%")->pluck('domain', 'id')->all() + : [] + ); + + $instance = Instance::find($id); + if(!$instance) { + $this->error('Oops, an error occured'); + exit; + } + + $tbl = [ + [ + $instance->domain, + number_format($instance->status_count), + number_format($instance->user_count), + ] + ]; + table( + ['Domain', 'Status Count', 'User Count'], + $tbl + ); + + $confirmed = confirm('Are you sure you want to unban this instance?'); + if(!$confirmed) { + $this->error('Aborting instance unban'); + exit; + } + + $instance->banned = false; + $instance->save(); + InstanceService::refresh(); + $this->info('Successfully un-banned ' . $instance->domain . '!'); + exit; + } + + protected function viewBannedInstances() + { + $data = Instance::whereBanned(true) + ->get(['domain', 'user_count', 'status_count']) + ->map(function($d) { + return [ + 'domain' => $d->domain, + 'user_count' => number_format($d->user_count), + 'status_count' => number_format($d->status_count), + ]; + }) + ->toArray(); + table( + ['Domain', 'User Count', 'Status Count'], + $data + ); + } + + protected function viewUnlistedInstances() + { + $data = Instance::whereUnlisted(true) + ->get(['domain', 'user_count', 'status_count', 'banned']) + ->map(function($d) { + return [ + 'domain' => $d->domain, + 'user_count' => number_format($d->user_count), + 'status_count' => number_format($d->status_count), + 'banned' => $d->banned ? '✅' : null + ]; + }) + ->toArray(); + table( + ['Domain', 'User Count', 'Status Count', 'Banned'], + $data + ); + } +} From 1f3f0cae65f6ef100e0710ddb0919e2a2ed986e8 Mon Sep 17 00:00:00 2001 From: Daniel Supernault Date: Wed, 7 Feb 2024 04:43:32 -0700 Subject: [PATCH 8/8] Update changelog --- CHANGELOG.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 591025bfd..640f6531a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -98,6 +98,12 @@ - Update AP helpers, fix sensitive bug ([00ed330c](https://github.com/pixelfed/pixelfed/commit/00ed330c)) - Update NotificationEpochUpdatePipeline, use more efficient query ([4d401389](https://github.com/pixelfed/pixelfed/commit/4d401389)) - Update notification pipelines, fix non-local saving ([fa97a1f3](https://github.com/pixelfed/pixelfed/commit/fa97a1f3)) +- Update NodeinfoService, disable redirects ([240e6bbe](https://github.com/pixelfed/pixelfed/commit/240e6bbe)) +- Update Instance model, add entity casts ([289cad47](https://github.com/pixelfed/pixelfed/commit/289cad47)) +- Update FetchNodeinfoPipeline, use more efficient dispatch ([ac01f51a](https://github.com/pixelfed/pixelfed/commit/ac01f51a)) +- Update horizon.php config ([1e3acade](https://github.com/pixelfed/pixelfed/commit/1e3acade)) +- Update PublicApiController, consume InstanceService blocked domains for account and statuses endpoints ([01b33fb3](https://github.com/pixelfed/pixelfed/commit/01b33fb3)) +- Update ApiV1Controller, enforce blocked instance domain logic ([5b284cac](https://github.com/pixelfed/pixelfed/commit/5b284cac)) - ([](https://github.com/pixelfed/pixelfed/commit/)) ## [v0.11.9 (2023-08-21)](https://github.com/pixelfed/pixelfed/compare/v0.11.8...v0.11.9)