diff --git a/app/Http/Controllers/Api/Remote/ActivityProcessingController.php b/app/Http/Controllers/Api/Remote/ActivityProcessingController.php new file mode 100644 index 000000000..30d7fa4e5 --- /dev/null +++ b/app/Http/Controllers/Api/Remote/ActivityProcessingController.php @@ -0,0 +1,115 @@ +getTimezone(); + + /** @var \Pterodactyl\Models\Node $node */ + $node = $request->attributes->get('node'); + + $servers = $node->servers()->whereIn('uuid', $request->servers())->get()->keyBy('uuid'); + $users = User::query()->whereIn('uuid', $request->users())->get()->keyBy('uuid'); + + clock()->log($request->input('data')); + + $logs = []; + foreach ($request->input('data') as $datum) { + /** @var \Pterodactyl\Models\Server|null $server */ + $server = $servers->get($datum['server']); + if (is_null($server) || is_null($event = $this->event($datum['event']))) { + continue; + } + + try { + $when = Carbon::createFromFormat( + Carbon::RFC3339, + preg_replace('/(\.\d+)Z$/', 'Z', $datum['timestamp']), + 'UTC' + ); + } catch (Exception $exception) { + Log::warning($exception, ['timestamp' => $datum['timestamp']]); + + // If we cannot parse the value for some reason don't blow up this request, just go ahead + // and log the event with the current time, and set the metadata value to have the original + // timestamp that was provided. + $when = Carbon::now(); + $datum['metadata'] = array_merge($datum['metadata'] ?? [], ['original_timestamp' => $datum['timestamp']]); + } + + $log = [ + 'ip' => empty($datum['ip']) ? '127.0.0.1' : $datum['ip'], + 'event' => $event, + 'properties' => json_encode($datum['metadata'] ?? []), + // We have to change the time to the current timezone due to the way Laravel is handling + // the date casting internally. If we just leave it in UTC it ends up getting double-cast + // and the time is way off. + 'timestamp' => $when->setTimezone($tz), + ]; + + if ($user = $users->get($datum['user'])) { + $log['actor_id'] = $user->id; + $log['actor_type'] = $user->getMorphClass(); + } + + if (!isset($logs[$datum['server']])) { + $logs[$datum['server']] = []; + } + + $logs[$datum['server']][] = $log; + } + + foreach ($logs as $key => $data) { + Assert::isInstanceOf($server = $servers->get($key), Server::class); + + $batch = []; + foreach ($data as $datum) { + $id = ActivityLog::insertGetId($datum); + $batch[] = [ + 'activity_log_id' => $id, + 'subject_id' => $server->id, + 'subject_type' => $server->getMorphClass(), + ]; + } + + ActivityLogSubject::insert($batch); + } + } + + /** + * Takes an event from Wings and converts it into the expected event type on + * the Panel. If no matching event type can be deduced, null is returned and + * the event won't be logged. + */ + protected function event(string $input): ?string + { + switch ($input) { + case 'console_command': + return 'server:console.command'; + case 'power_start': + return 'server:power.start'; + case 'power_stop': + return 'server:power.stop'; + case 'power_restart': + return 'server:power.restart'; + case 'power_kill': + return 'server:power.kill'; + default: + return null; + } + } +} diff --git a/app/Http/Requests/Api/Remote/ActivityEventRequest.php b/app/Http/Requests/Api/Remote/ActivityEventRequest.php new file mode 100644 index 000000000..795fe32e6 --- /dev/null +++ b/app/Http/Requests/Api/Remote/ActivityEventRequest.php @@ -0,0 +1,54 @@ + ['required', 'array'], + 'data.*' => ['array'], + 'data.*.user' => ['present', 'uuid'], + 'data.*.server' => ['required', 'uuid'], + 'data.*.event' => ['required', 'string'], + 'data.*.metadata' => ['present', 'nullable', 'array'], + 'data.*.ip' => ['present', 'ip'], + 'data.*.timestamp' => ['required', 'string'], + ]; + } + + /** + * Returns all of the unique server UUIDs that were recieved in this request. + * + * @return string[] + */ + public function servers(): array + { + return Collection::make($this->input('data'))->pluck('server')->unique()->toArray(); + } + + /** + * Returns all of the unique user UUIDs that were submitted in this request. + * + * @return string[] + */ + public function users(): array + { + return Collection::make($this->input('data')) + ->filter(function ($value) { + return !empty($value['user']); + }) + ->pluck('user') + ->unique() + ->toArray(); + } +} diff --git a/routes/api-remote.php b/routes/api-remote.php index e6ab18825..e43c62eaf 100644 --- a/routes/api-remote.php +++ b/routes/api-remote.php @@ -8,6 +8,7 @@ Route::post('/sftp/auth', Remote\SftpAuthenticationController::class); Route::get('/servers', [Remote\Servers\ServerDetailsController::class, 'list']); Route::post('/servers/reset', [Remote\Servers\ServerDetailsController::class, 'resetState']); +Route::post('/activity', Remote\ActivityProcessingController::class); Route::group(['prefix' => '/servers/{uuid}'], function () { Route::get('/', Remote\Servers\ServerDetailsController::class);