Make sure webhook messages can be versionised later.

This commit is contained in:
James Cole 2020-12-04 20:19:52 +01:00
parent 7ee9b51b3f
commit 48d1d5c90b
No known key found for this signature in database
GPG Key ID: B5669F9493CDE38D
10 changed files with 413 additions and 99 deletions

View File

@ -0,0 +1,57 @@
<?php
/*
* MessageGeneratorInterface.php
* Copyright (c) 2020 james@firefly-iii.org
*
* This file is part of Firefly III (https://github.com/firefly-iii).
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
namespace FireflyIII\Generator\Webhook;
use FireflyIII\User;
use Illuminate\Support\Collection;
/**
* Interface MessageGeneratorInterface
*/
interface MessageGeneratorInterface
{
/**
* @return int
*/
public function getVersion(): int;
/**
*
*/
public function generateMessages(): void;
/**
* @param User $user
*/
public function setUser(User $user): void;
/**
* @param Collection $transactionGroups
*/
public function setTransactionGroups(Collection $transactionGroups): void;
/**
* @param int $trigger
*/
public function setTrigger(int $trigger): void;
}

View File

@ -22,7 +22,6 @@
namespace FireflyIII\Generator\Webhook;
use FireflyIII\Events\RequestedSendWebhookMessages;
use FireflyIII\Events\StoredWebhookMessage;
use FireflyIII\Exceptions\FireflyException;
use FireflyIII\Models\Transaction;
use FireflyIII\Models\TransactionGroup;
@ -38,10 +37,11 @@ use Ramsey\Uuid\Uuid;
use Symfony\Component\HttpFoundation\ParameterBag;
/**
* Class WebhookMessageGenerator
* Class StandardMessageGenerator
*/
class WebhookMessageGenerator
class StandardMessageGenerator implements MessageGeneratorInterface
{
private int $version = 1;
private User $user;
private Collection $transactionGroups;
private int $trigger;
@ -89,6 +89,9 @@ class WebhookMessageGenerator
return $this->user->webhooks()->where('active', 1)->where('trigger', $this->trigger)->get(['webhooks.*']);
}
/**
* Will also trigger a send.
*/
private function run(): void
{
/** @var Webhook $webhook */
@ -100,8 +103,6 @@ class WebhookMessageGenerator
/**
* @param Webhook $webhook
*
* @throws FireflyException
*/
private function runWebhook(Webhook $webhook): void
{
@ -126,19 +127,28 @@ class WebhookMessageGenerator
'trigger' => config('firefly.webhooks.triggers')[$webhook->trigger],
'url' => $webhook->url,
'uuid' => $uuid->toString(),
'version' => 0,
'version' => sprintf('v%d',$this->getVersion()),
'response' => config('firefly.webhooks.responses')[$webhook->response],
'content' => [],
];
switch ($webhook->response) {
default:
throw new FireflyException(sprintf('Cannot handle this webhook response (%d)', $webhook->response));
Log::error(
sprintf('The response code for webhook #%d is "%d" and the message generator cant handle it. Soft fail.', $webhook->id, $webhook->response)
);
return;
case Webhook::RESPONSE_NONE:
$message['content'] = [];
break;
case Webhook::RESPONSE_TRANSACTIONS:
$transformer = new TransactionGroupTransformer;
$message['content'] = $transformer->transformObject($transactionGroup);
$transformer = new TransactionGroupTransformer;
try {
$message['content'] = $transformer->transformObject($transactionGroup);
} catch (FireflyException $e) {
$message['content'] = ['error' => 'Internal error prevented Firefly III from including data', 'message' => $e->getMessage()];
}
break;
case Webhook::RESPONSE_ACCOUNTS:
$accounts = $this->collectAccounts($transactionGroup);
@ -173,6 +183,8 @@ class WebhookMessageGenerator
/**
* @param Webhook $webhook
* @param array $message
*
* @return WebhookMessage
*/
private function storeMessage(Webhook $webhook, array $message): WebhookMessage
{
@ -188,4 +200,11 @@ class WebhookMessageGenerator
}
/**
* @inheritDoc
*/
public function getVersion(): int
{
return $this->version;
}
}

View File

@ -23,7 +23,7 @@ declare(strict_types=1);
namespace FireflyIII\Handlers\Events;
use FireflyIII\Events\StoredTransactionGroup;
use FireflyIII\Generator\Webhook\WebhookMessageGenerator;
use FireflyIII\Generator\Webhook\MessageGeneratorInterface;
use FireflyIII\Models\TransactionJournal;
use FireflyIII\Models\Webhook;
use FireflyIII\Repositories\Rule\RuleRepositoryInterface;
@ -82,7 +82,8 @@ class StoredGroupEventHandler
Log::debug('StoredTransactionGroup:triggerWebhooks');
$group = $storedGroupEvent->transactionGroup;
$user = $group->user;
$engine = new WebhookMessageGenerator;
/** @var MessageGeneratorInterface $engine */
$engine = app(MessageGeneratorInterface::class);
$engine->setUser($user);
$engine->setTransactionGroups(new Collection([$group]));
$engine->setTrigger(Webhook::TRIGGER_STORE_TRANSACTION);

View File

@ -23,7 +23,7 @@ declare(strict_types=1);
namespace FireflyIII\Handlers\Events;
use FireflyIII\Events\UpdatedTransactionGroup;
use FireflyIII\Generator\Webhook\WebhookMessageGenerator;
use FireflyIII\Generator\Webhook\MessageGeneratorInterface;
use FireflyIII\Models\Account;
use FireflyIII\Models\Transaction;
use FireflyIII\Models\TransactionJournal;
@ -122,7 +122,8 @@ class UpdatedGroupEventHandler
Log::debug('UpdatedGroupEventHandler:triggerWebhooks');
$group = $updatedGroupEvent->transactionGroup;
$user = $group->user;
$engine = new WebhookMessageGenerator;
/** @var MessageGeneratorInterface $engine */
$engine = app(MessageGeneratorInterface::class);
$engine->setUser($user);
$engine->setTransactionGroups(new Collection([$group]));
$engine->setTrigger(Webhook::TRIGGER_UPDATE_TRANSACTION);

View File

@ -23,8 +23,10 @@ namespace FireflyIII\Handlers\Events;
use Exception;
use FireflyIII\Helpers\Webhook\SignatureGeneratorInterface;
use FireflyIII\Models\WebhookAttempt;
use FireflyIII\Models\WebhookMessage;
use FireflyIII\Services\Webhook\WebhookSenderInterface;
use GuzzleHttp\Client;
use GuzzleHttp\Exception\ClientException;
use JsonException;
@ -36,97 +38,24 @@ use Log;
class WebhookEventHandler
{
/**
*
* Will try to send at most 3 messages so the flow doesn't get broken too much.
*/
public function sendWebhookMessages(): void
{
$max = (int)config('firefly.webhooks.max_attempts');
$max = 0 === $max ? 3 : $max;
$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) {
$count = $message->webhookAttempts()->count();
if ($count >= 3) {
Log::info('No send message.');
continue;
}
// TODO needs its own handler.
$this->sendMessageV0($message);
}
->get(['webhook_messages.*'])
->filter(
function (WebhookMessage $message) {
return $message->webhookAttempts()->count() <= 2;
}
)->splice(0, 3);
Log::debug(sprintf('Found %d webhook message(s) ready to be send.', $messages->count()));
$sender =app(WebhookSenderInterface::class);
$sender->setMessages($messages);
$sender->send();
}
/**
* @param WebhookMessage $message
*/
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) {
$attempt = new WebhookAttempt;
$attempt->webhookMessage()->associate($message);
$attempt->status_code = 0;
$attempt->logs = sprintf('Json error: %s', $e->getMessage());
$attempt->save();
return;
}
// 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);
// 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' => $signatureString,
'connect_timeout' => 3.14,
'User-Agent' => sprintf('FireflyIII/%s', config('firefly.version')),
'timeout' => 10,
],
];
$client = new Client;
$logs = $message->logs ?? [];
try {
$res = $client->request('POST', $message->webhook->url, $options);
$message->sent = true;
} catch (ClientException|Exception $e) {
Log::error($e->getMessage());
Log::error($e->getTraceAsString());
$logs[] = sprintf('%s: %s', date('Y-m-d H:i:s'), $e->getMessage());
$message->errored = true;
$message->sent = false;
}
$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()));
}
}

View File

@ -0,0 +1,70 @@
<?php
/*
* Sha3SignatureGenerator.php
* Copyright (c) 2020 james@firefly-iii.org
*
* This file is part of Firefly III (https://github.com/firefly-iii).
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
namespace FireflyIII\Helpers\Webhook;
use FireflyIII\Models\WebhookMessage;
use JsonException;
/**
* Class Sha3SignatureGenerator
*/
class Sha3SignatureGenerator implements SignatureGeneratorInterface
{
private int $version = 1;
/**
* @inheritDoc
*/
public function getVersion(): int
{
return $this->version;
}
/**
* @inheritDoc
*/
public function generate(WebhookMessage $message): string
{
try {
$json = json_encode($message->message, JSON_THROW_ON_ERROR);
} catch (JsonException $e) {
// TODO needs FireflyException.
return sprintf('t=1,v%d=err-invalid-signature', $this->getVersion());
}
// signature v1 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);
// 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 v1.
return sprintf('t=%s,v%d=%s', $timestamp, $this->getVersion(), $signature);
}
}

View File

@ -0,0 +1,44 @@
<?php
/*
* SignatureGeneratorInterface.php
* Copyright (c) 2020 james@firefly-iii.org
*
* This file is part of Firefly III (https://github.com/firefly-iii).
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
namespace FireflyIII\Helpers\Webhook;
use FireflyIII\Models\WebhookMessage;
/**
* Interface SignatureGeneratorInterface
*/
interface SignatureGeneratorInterface
{
/**
* Return the version of this signature generator.
*
* @return int
*/
public function getVersion(): int;
/**
* @param WebhookMessage $message
*
* @return string
*/
public function generate(WebhookMessage $message): string;
}

View File

@ -36,6 +36,8 @@ use FireflyIII\Helpers\Report\PopupReport;
use FireflyIII\Helpers\Report\PopupReportInterface;
use FireflyIII\Helpers\Report\ReportHelper;
use FireflyIII\Helpers\Report\ReportHelperInterface;
use FireflyIII\Helpers\Webhook\Sha3SignatureGenerator;
use FireflyIII\Helpers\Webhook\SignatureGeneratorInterface;
use FireflyIII\Repositories\ObjectGroup\ObjectGroupRepository;
use FireflyIII\Repositories\ObjectGroup\ObjectGroupRepositoryInterface;
use FireflyIII\Repositories\Telemetry\TelemetryRepository;
@ -50,6 +52,8 @@ use FireflyIII\Services\FireflyIIIOrg\Update\UpdateRequest;
use FireflyIII\Services\FireflyIIIOrg\Update\UpdateRequestInterface;
use FireflyIII\Services\Password\PwndVerifierV2;
use FireflyIII\Services\Password\Verifier;
use FireflyIII\Services\Webhook\StandardWebhookSender;
use FireflyIII\Services\Webhook\WebhookSenderInterface;
use FireflyIII\Support\Amount;
use FireflyIII\Support\ExpandedForm;
use FireflyIII\Support\FireflyConfig;
@ -226,6 +230,11 @@ class FireflyServiceProvider extends ServiceProvider
$this->app->bind(UpdateRequestInterface::class, UpdateRequest::class);
$this->app->bind(TelemetryRepositoryInterface::class, TelemetryRepository::class);
// webhooks:
$this->app->bind(SignatureGeneratorInterface::class,Sha3SignatureGenerator::class);
$this->app->bind(WebhookSenderInterface::class, StandardWebhookSender::class);
// password verifier thing
$this->app->bind(Verifier::class, PwndVerifierV2::class);

View File

@ -0,0 +1,138 @@
<?php
/*
* StandardWebhookSender.php
* Copyright (c) 2020 james@firefly-iii.org
*
* This file is part of Firefly III (https://github.com/firefly-iii).
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
namespace FireflyIII\Services\Webhook;
use FireflyIII\Exceptions\FireflyException;
use FireflyIII\Helpers\Webhook\SignatureGeneratorInterface;
use FireflyIII\Models\WebhookAttempt;
use FireflyIII\Models\WebhookMessage;
use GuzzleHttp\Client;
use GuzzleHttp\Exception\ClientException;
use Illuminate\Support\Collection;
use Log;
use JsonException;
/**
* Class StandardWebhookSender
*/
class StandardWebhookSender implements WebhookSenderInterface
{
private Collection $messages;
private int $version = 1;
/**
* @inheritDoc
*/
public function getVersion(): int
{
return $this->version;
}
/**
* @inheritDoc
*/
public function setMessages(Collection $messages): void
{
$this->messages = $messages;
}
/**
* @inheritDoc
*/
public function send(): void
{
/** @var WebhookMessage $message */
foreach ($this->messages as $message) {
try {
$this->sendMessage($message);
} catch (FireflyException $e) {
// TODO log attempt and make WebhookAttempt
}
}
}
/**
* @param WebhookMessage $message
*
* @throws \GuzzleHttp\Exception\GuzzleException
*/
private function sendMessage(WebhookMessage $message): void
{
// have the signature generator generate a signature. If it fails, the error thrown will
// end up in send() to be caught.
$signatureGenerator = app(SignatureGeneratorInterface::class);
$signature = $signatureGenerator->generate($message);
Log::debug(sprintf('Trying to send webhook message #%d', $message->id));
try {
$json = json_encode($message->message, JSON_THROW_ON_ERROR);
} catch (JsonException $e) {
// TODO throw Firefly Exception
// $attempt = new WebhookAttempt;
// $attempt->webhookMessage()->associate($message);
// $attempt->status_code = 0;
// $attempt->logs = sprintf('Json error: %s', $e->getMessage());
// $attempt->save();
return;
}
$options = [
'body' => $json,
'headers' => [
'Content-Type' => 'application/json',
'Accept' => 'application/json',
'Signature' => $signature,
'connect_timeout' => 3.14,
'User-Agent' => sprintf('FireflyIII/%s', config('firefly.version')),
'timeout' => 10,
],
];
$client = new Client;
//$logs = $message->logs ?? [];
try {
$res = $client->request('POST', $message->webhook->url, $options);
$message->sent = true;
} catch (ClientException|Exception $e) {
Log::error($e->getMessage());
Log::error($e->getTraceAsString());
//$logs[] = sprintf('%s: %s', date('Y-m-d H:i:s'), $e->getMessage());
$message->errored = true;
$message->sent = false;
}
$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()));
//$sender
//$this->sendMessageV0($message);
}
}

View File

@ -0,0 +1,46 @@
<?php
/*
* WebhookSenderInterface.php
* Copyright (c) 2020 james@firefly-iii.org
*
* This file is part of Firefly III (https://github.com/firefly-iii).
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
namespace FireflyIII\Services\Webhook;
use FireflyIII\Models\WebhookMessage;
use Illuminate\Support\Collection;
/**
* Interface WebhookSenderInterface
*/
interface WebhookSenderInterface
{
/**
* @return int
*/
public function getVersion(): int;
/**
* @param Collection $messages
*/
public function setMessages(Collection $messages): void;
/**
*
*/
public function send(): void;
}