2023-05-29 13:58:45 +00:00
|
|
|
<?php
|
|
|
|
|
|
|
|
|
|
namespace Appwrite\Platform\Workers;
|
|
|
|
|
|
|
|
|
|
use Exception;
|
|
|
|
|
use Utopia\App;
|
|
|
|
|
use Utopia\Database\Document;
|
2023-11-14 08:50:26 +00:00
|
|
|
use Utopia\Database\Database;
|
2023-05-29 13:58:45 +00:00
|
|
|
use Utopia\Platform\Action;
|
|
|
|
|
use Utopia\Queue\Message;
|
|
|
|
|
|
|
|
|
|
class Webhooks extends Action
|
|
|
|
|
{
|
2023-10-01 17:39:26 +00:00
|
|
|
private array $errors = [];
|
2024-01-05 09:59:07 +00:00
|
|
|
private const MAX_FAILED_ATTEMPTS = 10;
|
2023-05-29 13:58:45 +00:00
|
|
|
|
|
|
|
|
public static function getName(): string
|
|
|
|
|
{
|
|
|
|
|
return 'webhooks';
|
|
|
|
|
}
|
|
|
|
|
|
2023-06-04 16:25:56 +00:00
|
|
|
/**
|
|
|
|
|
* @throws Exception
|
|
|
|
|
*/
|
2023-05-29 13:58:45 +00:00
|
|
|
public function __construct()
|
|
|
|
|
{
|
|
|
|
|
$this
|
|
|
|
|
->desc('Webhooks worker')
|
|
|
|
|
->inject('message')
|
2023-11-14 08:50:26 +00:00
|
|
|
->inject('dbForConsole')
|
2023-11-20 14:19:15 +00:00
|
|
|
->callback(fn ($message, Database $dbForConsole) => $this->action($message, $dbForConsole));
|
2023-05-29 13:58:45 +00:00
|
|
|
}
|
|
|
|
|
|
2023-06-02 03:54:34 +00:00
|
|
|
/**
|
2023-10-01 17:39:26 +00:00
|
|
|
* @param Message $message
|
2023-11-14 08:50:26 +00:00
|
|
|
* @param Database $dbForConsole
|
2023-10-01 17:39:26 +00:00
|
|
|
* @return void
|
2023-06-02 03:54:34 +00:00
|
|
|
* @throws Exception
|
|
|
|
|
*/
|
2023-11-14 08:50:26 +00:00
|
|
|
public function action(Message $message, Database $dbForConsole): void
|
2023-05-29 13:58:45 +00:00
|
|
|
{
|
|
|
|
|
$payload = $message->getPayload() ?? [];
|
2023-06-04 08:19:49 +00:00
|
|
|
|
2023-05-29 13:58:45 +00:00
|
|
|
if (empty($payload)) {
|
|
|
|
|
throw new Exception('Missing payload');
|
2023-06-04 16:25:56 +00:00
|
|
|
}
|
2023-05-29 13:58:45 +00:00
|
|
|
|
2023-06-04 16:25:56 +00:00
|
|
|
$events = $payload['events'];
|
|
|
|
|
$webhookPayload = json_encode($payload['payload']);
|
|
|
|
|
$project = new Document($payload['project']);
|
|
|
|
|
$user = new Document($payload['user'] ?? []);
|
2023-05-29 13:58:45 +00:00
|
|
|
|
2023-06-04 16:25:56 +00:00
|
|
|
foreach ($project->getAttribute('webhooks', []) as $webhook) {
|
2024-01-05 09:43:54 +00:00
|
|
|
if (array_intersect($webhook->getAttribute('events', []), $events)) {
|
2023-11-14 08:50:26 +00:00
|
|
|
$this->execute($events, $webhookPayload, $webhook, $user, $project, $dbForConsole);
|
2023-05-29 13:58:45 +00:00
|
|
|
}
|
2023-06-04 16:25:56 +00:00
|
|
|
}
|
2023-05-29 13:58:45 +00:00
|
|
|
|
2023-10-01 17:39:26 +00:00
|
|
|
if (!empty($this->errors)) {
|
|
|
|
|
throw new Exception(\implode(" / \n\n", $this->errors));
|
2023-06-04 16:25:56 +00:00
|
|
|
}
|
2023-05-29 13:58:45 +00:00
|
|
|
}
|
|
|
|
|
|
2023-10-01 17:39:26 +00:00
|
|
|
/**
|
|
|
|
|
* @param array $events
|
|
|
|
|
* @param string $payload
|
|
|
|
|
* @param Document $webhook
|
|
|
|
|
* @param Document $user
|
|
|
|
|
* @param Document $project
|
2023-11-14 08:50:26 +00:00
|
|
|
* @param Database $dbForConsole
|
2023-10-01 17:39:26 +00:00
|
|
|
* @return void
|
|
|
|
|
*/
|
2023-11-14 08:50:26 +00:00
|
|
|
private function execute(array $events, string $payload, Document $webhook, Document $user, Document $project, Database $dbForConsole): void
|
2023-05-29 13:58:45 +00:00
|
|
|
{
|
2024-01-05 09:43:54 +00:00
|
|
|
if ($webhook->getAttribute('enabled') !== true) {
|
2023-11-14 08:50:26 +00:00
|
|
|
return;
|
|
|
|
|
}
|
2023-06-11 14:08:48 +00:00
|
|
|
|
2023-05-29 13:58:45 +00:00
|
|
|
$url = \rawurldecode($webhook->getAttribute('url'));
|
|
|
|
|
$signatureKey = $webhook->getAttribute('signatureKey');
|
|
|
|
|
$signature = base64_encode(hash_hmac('sha1', $url . $payload, $signatureKey, true));
|
|
|
|
|
$httpUser = $webhook->getAttribute('httpUser');
|
|
|
|
|
$httpPass = $webhook->getAttribute('httpPass');
|
|
|
|
|
$ch = \curl_init($webhook->getAttribute('url'));
|
2023-10-01 17:39:26 +00:00
|
|
|
|
2023-05-29 13:58:45 +00:00
|
|
|
\curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'POST');
|
|
|
|
|
\curl_setopt($ch, CURLOPT_POSTFIELDS, $payload);
|
|
|
|
|
\curl_setopt($ch, CURLOPT_HEADER, 0);
|
2024-01-06 18:52:28 +00:00
|
|
|
\curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
|
2023-11-14 08:50:26 +00:00
|
|
|
\curl_setopt($ch, CURLOPT_TIMEOUT, 15);
|
|
|
|
|
\curl_setopt($ch, CURLOPT_MAXFILESIZE, 5242880);
|
2023-05-29 13:58:45 +00:00
|
|
|
\curl_setopt($ch, CURLOPT_USERAGENT, \sprintf(
|
|
|
|
|
APP_USERAGENT,
|
|
|
|
|
App::getEnv('_APP_VERSION', 'UNKNOWN'),
|
|
|
|
|
App::getEnv('_APP_SYSTEM_SECURITY_EMAIL_ADDRESS', APP_EMAIL_SECURITY)
|
|
|
|
|
));
|
|
|
|
|
\curl_setopt(
|
|
|
|
|
$ch,
|
|
|
|
|
CURLOPT_HTTPHEADER,
|
|
|
|
|
[
|
2023-11-14 08:59:46 +00:00
|
|
|
'Content-Type: application/json',
|
|
|
|
|
'Content-Length: ' . \strlen($payload),
|
|
|
|
|
'X-' . APP_NAME . '-Webhook-Id: ' . $webhook->getId(),
|
|
|
|
|
'X-' . APP_NAME . '-Webhook-Events: ' . implode(',', $events),
|
|
|
|
|
'X-' . APP_NAME . '-Webhook-Name: ' . $webhook->getAttribute('name', ''),
|
|
|
|
|
'X-' . APP_NAME . '-Webhook-User-Id: ' . $user->getId(),
|
|
|
|
|
'X-' . APP_NAME . '-Webhook-Project-Id: ' . $project->getId(),
|
|
|
|
|
'X-' . APP_NAME . '-Webhook-Signature: ' . $signature,
|
2023-05-29 13:58:45 +00:00
|
|
|
]
|
|
|
|
|
);
|
2023-11-14 08:59:46 +00:00
|
|
|
curl_setopt($ch, CURLOPT_MAXREDIRS, 5);
|
2023-05-29 13:58:45 +00:00
|
|
|
|
|
|
|
|
if (!$webhook->getAttribute('security', true)) {
|
|
|
|
|
\curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false);
|
|
|
|
|
\curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (!empty($httpUser) && !empty($httpPass)) {
|
|
|
|
|
\curl_setopt($ch, CURLOPT_USERPWD, "$httpUser:$httpPass");
|
|
|
|
|
\curl_setopt($ch, CURLOPT_HTTPAUTH, CURLAUTH_BASIC);
|
|
|
|
|
}
|
|
|
|
|
|
2024-01-06 19:25:41 +00:00
|
|
|
$responseBody = \curl_exec($ch);
|
|
|
|
|
$curlError = \curl_error($ch);
|
|
|
|
|
$statusCode = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
|
|
|
|
|
\curl_close($ch);
|
2024-01-06 18:52:28 +00:00
|
|
|
|
2024-01-06 19:25:41 +00:00
|
|
|
if (!empty($curlError) || $statusCode >= 400) {
|
2023-12-12 21:30:16 +00:00
|
|
|
$dbForConsole->increaseDocumentAttribute('webhooks', $webhook->getId(), 'attempts', 1);
|
|
|
|
|
$webhook = $dbForConsole->getDocument('webhooks', $webhook->getId());
|
2023-12-14 20:15:17 +00:00
|
|
|
$attempts = $webhook->getAttribute('attempts');
|
2024-01-06 18:52:28 +00:00
|
|
|
|
2024-01-06 19:25:41 +00:00
|
|
|
$logs = '';
|
|
|
|
|
$logs .= 'URL: ' . $webhook->getAttribute('url') . "\n";
|
|
|
|
|
$logs .= 'Method: ' . 'POST' . "\n";
|
2024-01-06 18:52:28 +00:00
|
|
|
|
2024-01-06 19:25:41 +00:00
|
|
|
if (!empty($curlError)) {
|
|
|
|
|
$logs .= 'CURL Error: ' . $curlError . "\n";
|
|
|
|
|
$logs .= 'Events: ' . implode(', ', $events) . "\n";
|
2024-01-06 18:52:28 +00:00
|
|
|
} else {
|
2024-01-06 19:25:41 +00:00
|
|
|
$logs .= 'Status code: ' . $statusCode . "\n";
|
|
|
|
|
$logs .= 'Body: ' . "\n" . \mb_strcut($responseBody, 0, 10000) . "\n"; // Limit to 10kb
|
2024-01-06 18:52:28 +00:00
|
|
|
}
|
|
|
|
|
|
2023-12-14 20:15:17 +00:00
|
|
|
$webhook->setAttribute('logs', $logs);
|
2023-11-14 08:50:26 +00:00
|
|
|
|
2023-12-14 20:15:17 +00:00
|
|
|
if ($attempts >= self::MAX_FAILED_ATTEMPTS) {
|
2023-11-20 14:19:15 +00:00
|
|
|
$webhook->setAttribute('enabled', false);
|
2023-11-14 08:50:26 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$dbForConsole->updateDocument('webhooks', $webhook->getId(), $webhook);
|
|
|
|
|
$dbForConsole->deleteCachedDocument('projects', $project->getId());
|
|
|
|
|
|
2023-12-14 20:15:17 +00:00
|
|
|
$this->errors[] = $logs;
|
2024-01-05 12:24:20 +00:00
|
|
|
} else {
|
2024-01-06 18:52:28 +00:00
|
|
|
$webhook->setAttribute('attempts', 0); // Reset attempts on success
|
2024-01-06 19:25:41 +00:00
|
|
|
$dbForConsole->updateDocument('webhooks', $webhook->getId(), $webhook);
|
|
|
|
|
$dbForConsole->deleteCachedDocument('projects', $project->getId());
|
2023-05-29 13:58:45 +00:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|