From 7ee9b51b3f4d6784b0ac2de612d9debaf2988e6f Mon Sep 17 00:00:00 2001 From: James Cole Date: Fri, 4 Dec 2020 06:21:22 +0100 Subject: [PATCH] Split webhook message sending into different models. --- .../Webhook/WebhookMessageGenerator.php | 6 +- app/Handlers/Events/WebhookEventHandler.php | 71 ++++++++++++------- config/firefly.php | 1 + .../2020_11_12_070604_changes_for_v550.php | 26 +++++-- 4 files changed, 72 insertions(+), 32 deletions(-) diff --git a/app/Generator/Webhook/WebhookMessageGenerator.php b/app/Generator/Webhook/WebhookMessageGenerator.php index e35eedc0a4..5efba90aa0 100644 --- a/app/Generator/Webhook/WebhookMessageGenerator.php +++ b/app/Generator/Webhook/WebhookMessageGenerator.php @@ -100,6 +100,8 @@ class WebhookMessageGenerator /** * @param Webhook $webhook + * + * @throws FireflyException */ private function runWebhook(Webhook $webhook): void { @@ -124,6 +126,7 @@ class WebhookMessageGenerator 'trigger' => config('firefly.webhooks.triggers')[$webhook->trigger], 'url' => $webhook->url, 'uuid' => $uuid->toString(), + 'version' => 0, 'response' => config('firefly.webhooks.responses')[$webhook->response], 'content' => [], ]; @@ -131,6 +134,8 @@ class WebhookMessageGenerator switch ($webhook->response) { default: throw new FireflyException(sprintf('Cannot handle this webhook response (%d)', $webhook->response)); + case Webhook::RESPONSE_NONE: + $message['content'] = []; case Webhook::RESPONSE_TRANSACTIONS: $transformer = new TransactionGroupTransformer; $message['content'] = $transformer->transformObject($transactionGroup); @@ -177,7 +182,6 @@ class WebhookMessageGenerator $webhookMessage->errored = false; $webhookMessage->uuid = $message['uuid']; $webhookMessage->message = $message; - $webhookMessage->logs = null; $webhookMessage->save(); return $webhookMessage; diff --git a/app/Handlers/Events/WebhookEventHandler.php b/app/Handlers/Events/WebhookEventHandler.php index e3ac77f257..7176bab798 100644 --- a/app/Handlers/Events/WebhookEventHandler.php +++ b/app/Handlers/Events/WebhookEventHandler.php @@ -23,6 +23,7 @@ namespace FireflyIII\Handlers\Events; use Exception; +use FireflyIII\Models\WebhookAttempt; use FireflyIII\Models\WebhookMessage; use GuzzleHttp\Client; use GuzzleHttp\Exception\ClientException; @@ -41,56 +42,69 @@ class WebhookEventHandler { $max = (int)config('firefly.webhooks.max_attempts'); $max = 0 === $max ? 3 : $max; - $messages = WebhookMessage::where('sent', 0) - ->where('attempts', '<=', $max) - ->get(); - Log::debug(sprintf('Going to send %d webhook message(s)', $messages->count())); + $messages = WebhookMessage + ::where('webhook_messages.sent', 0) + ->where('webhook_messages.errored', 0) + ->get(['webhook_messages.*']); + Log::debug(sprintf('Found %d webhook message(s) to be send.', $messages->count())); /** @var WebhookMessage $message */ foreach ($messages as $message) { - $this->sendMessage($message); + $count = $message->webhookAttempts()->count(); + if ($count >= 3) { + Log::info('No send message.'); + continue; + } + // TODO needs its own handler. + $this->sendMessageV0($message); } } /** * @param WebhookMessage $message */ - private function sendMessage(WebhookMessage $message): void + private function sendMessageV0(WebhookMessage $message): void { Log::debug(sprintf('Trying to send webhook message #%d', $message->id)); try { $json = json_encode($message->message, JSON_THROW_ON_ERROR); } catch (JsonException $e) { - $message->attempts++; - $message->logs[] = sprintf('%s: %s', date('Y-m-d H:i:s'), sprintf('Json error: %s', $e->getMessage())); - $message->save(); + $attempt = new WebhookAttempt; + $attempt->webhookMessage()->associate($message); + $attempt->status_code = 0; + $attempt->logs = sprintf('Json error: %s', $e->getMessage()); + $attempt->save(); return; } - $user = $message->webhook->user; - try { - $token = $user->generateAccessToken(); - } catch (Exception $e) { - $message->attempts++; - $message->logs[] = sprintf('%s: %s', date('Y-m-d H:i:s'), sprintf('Could not generate token: %s', $e->getMessage())); - $message->save(); + // signature v0 is generated using the following structure: + // The signed_payload string is created by concatenating: + // The timestamp (as a string) + // The character . + // The character . + // The actual JSON payload (i.e., the request body) + $timestamp = time(); + $payload = sprintf('%s.%s', $timestamp, $json); + $signature = hash_hmac('sha3-256', $payload, $message->webhook->secret, false); - return; - } - $accessToken = app('preferences')->getForUser($user, 'access_token', $token); - $signature = hash_hmac('sha3-256', $json, $accessToken->data, false); - $options = [ + // signature string: + // header included in each signed event contains a timestamp and one or more signatures. + // The timestamp is prefixed by t=, and each signature is prefixed by a scheme. + // Schemes start with v, followed by an integer. Currently, the only valid live signature scheme is v0. + $signatureString = sprintf('t=%s,v0=%s', $timestamp, $signature); + + $options = [ 'body' => $json, 'headers' => [ 'Content-Type' => 'application/json', 'Accept' => 'application/json', - 'Signature' => $signature, + 'Signature' => $signatureString, 'connect_timeout' => 3.14, 'User-Agent' => sprintf('FireflyIII/%s', config('firefly.version')), 'timeout' => 10, ], ]; - $client = new Client; - $logs = $message->logs ?? []; + $client = new Client; + $logs = $message->logs ?? []; try { $res = $client->request('POST', $message->webhook->url, $options); $message->sent = true; @@ -101,10 +115,15 @@ class WebhookEventHandler $message->errored = true; $message->sent = false; } - $message->attempts++; - $message->logs = $logs; $message->save(); + $attempt = new WebhookAttempt; + $attempt->webhookMessage()->associate($message); + $attempt->status_code = $res->getStatusCode(); + $attempt->logs = ''; + $attempt->response = (string)$res->getBody(); + $attempt->save(); + Log::debug(sprintf('Webhook message #%d was sent. Status code %d', $message->id, $res->getStatusCode())); Log::debug(sprintf('Webhook request body size: %d bytes', strlen($json))); Log::debug(sprintf('Response body: %s', $res->getBody())); diff --git a/config/firefly.php b/config/firefly.php index 76eac98c40..54207bbc5d 100644 --- a/config/firefly.php +++ b/config/firefly.php @@ -832,6 +832,7 @@ return [ 'responses' => [ 200 => 'RESPONSE_TRANSACTIONS', 210 => 'RESPONSE_ACCOUNTS', + 220 => 'RESPONSE_NONE', ], 'deliveries' => [ 300 => 'DELIVERY_JSON', diff --git a/database/migrations/2020_11_12_070604_changes_for_v550.php b/database/migrations/2020_11_12_070604_changes_for_v550.php index 12905abc62..c4992b449b 100644 --- a/database/migrations/2020_11_12_070604_changes_for_v550.php +++ b/database/migrations/2020_11_12_070604_changes_for_v550.php @@ -119,13 +119,13 @@ class ChangesForV550 extends Migration $table->softDeletes(); $table->integer('user_id', false, true); $table->string('title', 512)->index(); + $table->string('secret', 32)->index(); $table->boolean('active')->default(true); $table->unsignedSmallInteger('trigger', false); $table->unsignedSmallInteger('response', false); $table->unsignedSmallInteger('delivery', false); - $table->string('url', 512)->index(); + $table->string('url', 1024); $table->foreign('user_id')->references('id')->on('users')->onDelete('cascade'); - $table->unique(['user_id', 'trigger', 'response', 'delivery', 'url']); $table->unique(['user_id', 'title']); } ); @@ -137,15 +137,31 @@ class ChangesForV550 extends Migration $table->increments('id'); $table->timestamps(); $table->softDeletes(); - $table->integer('webhook_id', false, true); $table->boolean('sent')->default(false); $table->boolean('errored')->default(false); - $table->unsignedTinyInteger('attempts')->default(0); + + $table->integer('webhook_id', false, true); $table->string('uuid', 64); $table->longText('message'); - $table->longText('logs')->nullable(); + $table->foreign('webhook_id')->references('id')->on('webhooks')->onDelete('cascade'); } ); + + Schema::create( + 'webhook_attempts', + static function (Blueprint $table) { + $table->increments('id'); + $table->timestamps(); + $table->softDeletes(); + $table->integer('webhook_message_id', false, true); + $table->unsignedSmallInteger('status_code')->default(0); + + $table->longText('logs')->nullable(); + $table->longText('response')->nullable(); + + $table->foreign('webhook_message_id')->references('id')->on('webhook_messages')->onDelete('cascade'); + } + ); } }