| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202 |
- <?php
- namespace App\Http\Controllers;
- use App\Services\WebhookService;
- use Illuminate\Http\JsonResponse;
- use Illuminate\Http\Request;
- use Illuminate\Support\Facades\Log;
- class WebhookController extends Controller
- {
- public function __construct(
- private readonly WebhookService $webhookService,
- ) {}
- public function pagarme(Request $request): JsonResponse
- {
- if (! $this->validPagarmeCredentials($request)) {
- return $this->errorResponse(message: __('http.unauthorized_token'), code: 401);
- }
- $this->webhookService->handlePagarme($request->all());
- return $this->successResponse(message: __('http.webhook_received'));
- }
- public function didit(Request $request): JsonResponse
- {
- $payload = $request->all();
- Log::channel('didit')->info('Webhook recebido do Didit', [
- 'event_id' => data_get($payload, 'event_id'),
- 'webhook_type' => data_get($payload, 'webhook_type'),
- 'session_id' => data_get($payload, 'session_id'),
- 'status' => data_get($payload, 'status'),
- 'vendor_data' => data_get($payload, 'vendor_data'),
- 'environment' => data_get($payload, 'environment'),
- 'is_test' => $request->hasHeader('X-Didit-Test-Webhook'),
- ]);
- if (! $this->validDiditSignature($request)) {
- return $this->errorResponse(message: __('http.unauthorized_token'), code: 401);
- }
- $this->webhookService->handleDidit($payload);
- return $this->successResponse(message: __('http.webhook_received'));
- }
- //
- private function validPagarmeCredentials(Request $request): bool
- {
- $configuredUser = config('services.pagarme.webhook_user');
- $configuredPassword = config('services.pagarme.webhook_password');
- if (empty($configuredUser) || empty($configuredPassword)) {
- return false;
- }
- return is_string($configuredUser)
- && is_string($configuredPassword)
- && $this->validBasicAuthCredentials($request, $configuredUser, $configuredPassword);
- }
- private function validBasicAuthCredentials(Request $request, string $configuredUser, string $configuredPassword): bool
- {
- $receivedUser = $request->getUser();
- $receivedPassword = $request->getPassword();
- return is_string($receivedUser)
- && is_string($receivedPassword)
- && hash_equals($configuredUser, $receivedUser)
- && hash_equals($configuredPassword, $receivedPassword);
- }
- private function validDiditSignature(Request $request): bool
- {
- $secrets = $this->diditWebhookSecrets();
- if ($secrets === []) {
- Log::channel('didit')->error('DIDIT_WEBHOOK_SECRET nao configurado; webhook rejeitado');
- return false;
- }
- if (! $this->validDiditTimestamp($request)) {
- return false;
- }
- $rawBody = $request->getContent();
- $candidates = array_filter([
- 'X-Signature-V2' => $this->diditCanonicalBody($rawBody),
- 'X-Signature' => $rawBody,
- ], static fn ($body) => is_string($body));
- foreach ($candidates as $header => $body) {
- $received = (string) $request->header($header, '');
- if ($received === '') {
- continue;
- }
- foreach ($secrets as $secret) {
- if (hash_equals(hash_hmac('sha256', $body, $secret), $received)) {
- return true;
- }
- }
- }
- Log::channel('didit')->warning('Assinatura do webhook Didit invalida', [
- 'secrets_testados' => count($secrets),
- 'received_v2' => substr((string) $request->header('X-Signature-V2', ''), 0, 12),
- 'body_length' => strlen($rawBody),
- ]);
- return false;
- }
- /**
- * @return list<string>
- */
- private function diditWebhookSecrets(): array
- {
- $configured = config('services.didit.webhook_secret');
- if (! is_string($configured)) {
- return [];
- }
- return array_values(array_filter(
- array_map('trim', explode(',', $configured)),
- static fn (string $secret) => $secret !== '',
- ));
- }
- private function validDiditTimestamp(Request $request): bool
- {
- $timestamp = $request->header('X-Timestamp');
- if (! is_numeric($timestamp)) {
- Log::channel('didit')->warning('Webhook Didit sem X-Timestamp valido');
- return false;
- }
- $tolerance = (int) config('services.didit.webhook_tolerance', 300);
- if (abs(time() - (int) $timestamp) > $tolerance) {
- Log::channel('didit')->warning('Webhook Didit fora da janela de tolerancia', [
- 'timestamp' => (int) $timestamp,
- 'now' => time(),
- ]);
- return false;
- }
- return true;
- }
- private function diditCanonicalBody(string $rawBody): ?string
- {
- try {
- $decoded = json_decode($rawBody, false, 512, JSON_THROW_ON_ERROR);
- } catch (\JsonException) {
- return null;
- }
- $canonical = json_encode(
- $this->canonicalizeDiditValue($decoded),
- JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE,
- );
- return $canonical === false ? null : $canonical;
- }
- private function canonicalizeDiditValue(mixed $value): mixed
- {
- if (is_array($value)) {
- return array_map(fn ($item) => $this->canonicalizeDiditValue($item), $value);
- }
- if ($value instanceof \stdClass) {
- $data = get_object_vars($value);
- ksort($data, SORT_STRING);
- foreach ($data as $key => $item) {
- $data[$key] = $this->canonicalizeDiditValue($item);
- }
- return (object) $data;
- }
- if (is_float($value) && is_finite($value) && floor($value) === $value && abs($value) < PHP_INT_MAX) {
- return (int) $value;
- }
- return $value;
- }
- }
|