. */ declare(strict_types=1); namespace FireflyIII\Services\Spectre\Request; use Exception; use FireflyIII\Exceptions\FireflyException; use FireflyIII\User; use GuzzleHttp\Client; use GuzzleHttp\Exception\GuzzleException; use Log; use RuntimeException; /** * Class SpectreRequest */ abstract class SpectreRequest { /** @var int */ protected $expiresAt = 0; /** @var string */ private $appId; /** @var string */ private $privateKey; /** @var string */ private $secret; /** @var string */ private $server; /** @var User */ private $user; /** * */ abstract public function call(): void; /** * @codeCoverageIgnore * @return string */ public function getAppId(): string { return $this->appId; } /** * @codeCoverageIgnore * * @param string $appId */ public function setAppId(string $appId): void { $this->appId = $appId; } /** * @codeCoverageIgnore * @return string */ public function getSecret(): string { return $this->secret; } /** * @codeCoverageIgnore * * @param string $secret */ public function setSecret(string $secret): void { $this->secret = $secret; } /** * @codeCoverageIgnore * @return string */ public function getServer(): string { return $this->server; } /** * @codeCoverageIgnore * * @param string $privateKey */ public function setPrivateKey(string $privateKey): void { $this->privateKey = $privateKey; } /** * @param User $user */ public function setUser(User $user): void { $this->user = $user; $this->server = 'https://' . config('import.options.spectre.server'); $this->expiresAt = time() + 180; $privateKey = app('preferences')->getForUser($user, 'spectre_private_key', null); $this->privateKey = $privateKey->data; // set client ID $appId = app('preferences')->getForUser($user, 'spectre_app_id', null); if (null !== $appId && '' !== (string)$appId->data) { $this->appId = $appId->data; } // set service secret $secret = app('preferences')->getForUser($user, 'spectre_secret', null); if (null !== $secret && '' !== (string)$secret->data) { $this->secret = $secret->data; } } /** * @param string $method * @param string $uri * @param string $data * * @return string * * @throws FireflyException */ protected function generateSignature(string $method, string $uri, string $data): string { if ('' === $this->privateKey) { throw new FireflyException('No private key present.'); } $method = strtolower($method); if ('get' === $method || 'delete' === $method) { $data = ''; } $toSign = $this->expiresAt . '|' . strtoupper($method) . '|' . $uri . '|' . $data . ''; // no file so no content there. Log::debug(sprintf('String to sign: "%s"', $toSign)); $signature = ''; // Sign the data openssl_sign($toSign, $signature, $this->privateKey, OPENSSL_ALGO_SHA256); $signature = base64_encode($signature); return $signature; } /** * @return array */ protected function getDefaultHeaders(): array { $userAgent = sprintf('FireflyIII v%s', config('firefly.version')); return [ 'App-id' => $this->getAppId(), 'Secret' => $this->getSecret(), 'Accept' => 'application/json', 'Content-type' => 'application/json', 'Cache-Control' => 'no-cache', 'User-Agent' => $userAgent, 'Expires-at' => $this->expiresAt, ]; } /** * @param string $uri * @param array $data * * @return array * * @throws FireflyException */ protected function sendSignedSpectreGet(string $uri, array $data): array { if ('' === $this->server) { throw new FireflyException('No Spectre server defined'); } $headers = $this->getDefaultHeaders(); $sendBody = json_encode($data); // OK $fullUri = $this->server . $uri; $signature = $this->generateSignature('get', $fullUri, $sendBody); $headers['Signature'] = $signature; Log::debug('Final headers for spectre signed get request:', $headers); try { $client = new Client; $res = $client->request('GET', $fullUri, ['headers' => $headers]); } catch (GuzzleException|Exception $e) { throw new FireflyException(sprintf('Guzzle Exception: %s', $e->getMessage())); } $statusCode = $res->getStatusCode(); try { $returnBody = $res->getBody()->getContents(); } catch (RunTimeException $e) { Log::error(sprintf('Could not get body from SpectreRequest::GET result: %s', $e->getMessage())); $returnBody = ''; } $this->detectError($returnBody, $statusCode); $array = json_decode($returnBody, true); $responseHeaders = $res->getHeaders(); $array['ResponseHeaders'] = $responseHeaders; $array['ResponseStatusCode'] = $statusCode; if (isset($array['error_class'])) { $message = $array['error_message'] ?? '(no message)'; throw new FireflyException(sprintf('Error of class %s: %s', $array['error_class'], $message)); } return $array; } /** * @param string $uri * @param array $data * * @return array * * @throws FireflyException */ protected function sendSignedSpectrePost(string $uri, array $data): array { if ('' === $this->server) { throw new FireflyException('No Spectre server defined'); } $headers = $this->getDefaultHeaders(); $body = json_encode($data); $fullUri = $this->server . $uri; $signature = $this->generateSignature('post', $fullUri, $body); $headers['Signature'] = $signature; Log::debug('Final headers for spectre signed POST request:', $headers); try { $client = new Client; $res = $client->request('POST', $fullUri, ['headers' => $headers, 'body' => $body]); } catch (GuzzleException|Exception $e) { throw new FireflyException(sprintf('Guzzle Exception: %s', $e->getMessage())); } try { $body = $res->getBody()->getContents(); } catch (RunTimeException $e) { Log::error(sprintf('Could not get body from SpectreRequest::POST result: %s', $e->getMessage())); $body = ''; } $statusCode = $res->getStatusCode(); $this->detectError($body, $statusCode); $array = json_decode($body, true); $responseHeaders = $res->getHeaders(); $array['ResponseHeaders'] = $responseHeaders; $array['ResponseStatusCode'] = $statusCode; return $array; } /** * @param string $body * * @param int $statusCode * * @throws FireflyException */ private function detectError(string $body, int $statusCode): void { $array = json_decode($body, true); if (isset($array['error_class'])) { $message = $array['error_message'] ?? '(no message)'; $errorClass = $array['error_class']; $class = sprintf('\\FireflyIII\\Services\\Spectre\Exception\\%sException', $errorClass); if (class_exists($class)) { throw new $class($message); } throw new FireflyException(sprintf('Error of class %s: %s', $errorClass, $message)); } if (200 !== $statusCode) { throw new FireflyException(sprintf('Status code %d: %s', $statusCode, $body)); } } }