From e0741879d50f3ea626c056cfb5e68981ced88766 Mon Sep 17 00:00:00 2001 From: s4ddly <110701663+s4ddly@users.noreply.github.com> Date: Tue, 5 Dec 2023 10:25:39 +0100 Subject: [PATCH] Extract logging logic outside Util class (#60) --- composer.json | 4 +- src/Utilities/FileLogger.php | 70 ++++++++ src/Utilities/Logger.php | 75 ++++++++ src/Utilities/Util.php | 58 ++---- src/Utilities/phpseclib/Crypt/RSA.php | 13 ++ src/Utilities/phpseclib/File/X509.php | 28 +++ .../JWSVerifiedPaymentNotification.php | 167 ++++++++++++++++++ 7 files changed, 369 insertions(+), 46 deletions(-) create mode 100644 src/Utilities/FileLogger.php create mode 100644 src/Utilities/Logger.php create mode 100644 src/Utilities/phpseclib/Crypt/RSA.php create mode 100644 src/Utilities/phpseclib/File/X509.php create mode 100644 src/Webhook/JWSVerifiedPaymentNotification.php diff --git a/composer.json b/composer.json index e6aa11c..aa00bbe 100644 --- a/composer.json +++ b/composer.json @@ -14,7 +14,9 @@ }, "require": { "php": ">=5.6.0", - "ext-curl": "*" + "ext-curl": "*", + "ext-json": "*", + "phpseclib/phpseclib": "^2 || ^3" }, "require-dev": { "php-parallel-lint/php-parallel-lint": "^1.3.2", diff --git a/src/Utilities/FileLogger.php b/src/Utilities/FileLogger.php new file mode 100644 index 0000000..3f3768e --- /dev/null +++ b/src/Utilities/FileLogger.php @@ -0,0 +1,70 @@ +logFilePath = $logFilePath; + } + + public function log($level, $message, array $context = []) + { + $this->info($message); + } + + /** + * @param string $message + * + * @throws TException + */ + public function info($message) + { + $content = json_decode($message, true); + $logText = PHP_EOL.'==========================='; + $logText .= PHP_EOL.$content['title']; + $logText .= PHP_EOL.'==========================='; + $logText .= PHP_EOL.$content['date']; + $logText .= PHP_EOL.'ip: '.$content['ip']; + $logText .= PHP_EOL; + $logText .= $content['message']; + $logText .= PHP_EOL; + + $this->checkLogFile(); + file_put_contents($this->getLogPath(), $logText, FILE_APPEND); + } + + /** @return string */ + private function getLogPath() + { + $logFileName = sprintf('log_%s.log', date('Y-m-d')); + + if (null !== $this->logFilePath) { + $logPath = $this->logFilePath.$logFileName; + } else { + $logPath = sprintf('%s/../Logs/%s', __DIR__, $logFileName); + } + + return $logPath; + } + + /** @throws TException */ + private function checkLogFile() + { + $logFilePath = $this->getLogPath(); + + if (!file_exists($logFilePath)) { + file_put_contents($logFilePath, ' '.PHP_EOL); + chmod($logFilePath, 0644); + } + + if (!file_exists($logFilePath) || !is_writable($logFilePath)) { + throw new TException('Unable to create or write the log file'); + } + } +} diff --git a/src/Utilities/Logger.php b/src/Utilities/Logger.php new file mode 100644 index 0000000..99e79a0 --- /dev/null +++ b/src/Utilities/Logger.php @@ -0,0 +1,75 @@ + $ip, + 'title' => $title, + 'date' => date('Y-m-d H:i:s'), + 'message' => $text, + 'logLevel' => $logLevel, + ]; + + self::getLogger()->log($logLevel, json_encode($content)); + } + + /** @param string $text */ + public static function logLine($text) + { + self::$logger->info((string) $text); + } +} diff --git a/src/Utilities/Util.php b/src/Utilities/Util.php index 301d4c0..ab710b3 100644 --- a/src/Utilities/Util.php +++ b/src/Utilities/Util.php @@ -2,8 +2,6 @@ namespace Tpay\OriginApi\Utilities; -use Exception; - /** * Utility class which helps with: * - parsing template files @@ -79,22 +77,7 @@ public static function parseTemplate($templateFileName, $data = []) */ public static function log($title, $text) { - $text = (string) $text; - $logFilePath = self::getLogPath(); - $ip = (isset($_SERVER[static::REMOTE_ADDR])) ? $_SERVER[static::REMOTE_ADDR] : ''; - - $logText = PHP_EOL.'==========================='; - $logText .= PHP_EOL.$title; - $logText .= PHP_EOL.'==========================='; - $logText .= PHP_EOL.date('Y-m-d H:i:s'); - $logText .= PHP_EOL.'ip: '.$ip; - $logText .= PHP_EOL; - $logText .= $text; - $logText .= PHP_EOL.PHP_EOL; - - if (true === static::$loggingEnabled) { - file_put_contents($logFilePath, $logText, FILE_APPEND); - } + Logger::log($title, $text); } /** @@ -104,11 +87,18 @@ public static function log($title, $text) */ public static function logLine($text) { - $text = (string) $text; - $logFilePath = self::getLogPath(); - if (true === static::$loggingEnabled) { - file_put_contents($logFilePath, PHP_EOL.$text, FILE_APPEND); - } + Logger::logLine($text); + } + + /** + * @param float|int $number + * @param int $decimals + * + * @return string + */ + public static function numberFormat($number, $decimals = 2) + { + return number_format($number, $decimals, '.', ''); } /** @@ -161,26 +151,4 @@ public function setPath($path) return $this; } - - private static function getLogPath() - { - if (false === static::$loggingEnabled) { - return; - } - $logFileName = 'log_'.date('Y-m-d').'.php'; - if (!empty(static::$customLogPatch)) { - $logPath = static::$customLogPatch.$logFileName; - } else { - $logPath = dirname(__FILE__).'/../../Logs/'.$logFileName; - } - if (!file_exists($logPath)) { - file_put_contents($logPath, ' '.PHP_EOL); - chmod($logPath, 0644); - } - if (!file_exists($logPath) || !is_writable($logPath)) { - throw new Exception('Unable to create or write the log file'); - } - - return $logPath; - } } diff --git a/src/Utilities/phpseclib/Crypt/RSA.php b/src/Utilities/phpseclib/Crypt/RSA.php new file mode 100644 index 0000000..5fa6900 --- /dev/null +++ b/src/Utilities/phpseclib/Crypt/RSA.php @@ -0,0 +1,13 @@ +withHash($hash)->withPadding($signature); + } + } +} elseif (class_exists('phpseclib\File\X509')) { + class X509 extends \phpseclib\File\X509 + { + public function withSettings($publicKey, $hash, $signature) + { + $publicKey->setHash($hash); + $publicKey->setSignatureMode($signature); + + return $publicKey; + } + } +} else { + throw new RuntimeException('Cannot find supported phpseclib/phpseclib library'); +} diff --git a/src/Webhook/JWSVerifiedPaymentNotification.php b/src/Webhook/JWSVerifiedPaymentNotification.php new file mode 100644 index 0000000..0d66778 --- /dev/null +++ b/src/Webhook/JWSVerifiedPaymentNotification.php @@ -0,0 +1,167 @@ +productionMode = $productionMode; + $this->merchantSecret = $merchantSecret; + } + + /** + * Get checked notification object. + * If exception occurs it means that something went wrong with notification verification process. + * + * @throws TException + * + * @return array + */ + public function getNotification() + { + $notification = $this->getNotificationObject(); + + $this->checkMd5($notification); + $this->checkJwsSignature(); + + return $notification; + } + + protected function checkJwsSignature() + { + $jws = isset($_SERVER['HTTP_X_JWS_SIGNATURE']) ? $_SERVER['HTTP_X_JWS_SIGNATURE'] : null; + + if (null === $jws) { + throw new TException('Missing JSW header'); + } + + $jwsData = explode('.', $jws); + $headers = isset($jwsData[0]) ? $jwsData[0] : null; + $signature = isset($jwsData[2]) ? $jwsData[2] : null; + + if (null === $headers || null === $signature) { + throw new TException('Invalid JWS header'); + } + + $headersJson = base64_decode(strtr($headers, '-_', '+/')); + + /** @var array $headersData */ + $headersData = json_decode($headersJson, true); + + /** @var null|string $x5u */ + $x5u = isset($headersData['x5u']) ? $headersData['x5u'] : null; + + if (null === $x5u) { + throw new TException('Missing x5u header'); + } + + $prefix = $this->getResourcePrefix(); + + if (substr($x5u, 0, strlen($prefix)) !== $prefix) { + throw new TException('Wrong x5u url'); + } + + $certificate = file_get_contents($x5u); + $trusted = file_get_contents($this->getResourcePrefix().'/x509/tpay-jws-root.pem'); + + $x509 = new X509(); + $x509->loadX509($certificate); + $x509->loadCA($trusted); + + if (!$x509->validateSignature()) { + throw new TException('Signing certificate is not signed by Tpay CA certificate'); + } + + $body = file_get_contents('php://input'); + $payload = str_replace('=', '', strtr(base64_encode($body), '+/', '-_')); + $decodedSignature = base64_decode(strtr($signature, '-_', '+/')); + $publicKey = $x509->getPublicKey(); + + /** + * @phpstan-ignore-next-line + */ + $publicKey = $x509->withSettings($publicKey, 'sha256', RSA::SIGNATURE_PKCS1); + if (!$publicKey->verify($headers.'.'.$payload, $decodedSignature)) { + throw new TException('FALSE - Invalid JWS signature'); + } + } + + /** + * @param int $id + * @param string $transactionId + * @param string $amount + * @param string $orderId + * @param string $merchantSecret + * @param string $requestMd5 + * + * @throws TException + */ + private function checkMd5Validity($id, $transactionId, $amount, $orderId, $merchantSecret, $requestMd5) + { + if (md5($id.$transactionId.$amount.$orderId.$merchantSecret) !== $requestMd5) { + throw new TException('MD5 checksum is invalid'); + } + } + + /** + * @param mixed $notification + * + * @throws TException + */ + private function checkMd5($notification) + { + $this->checkMd5Validity( + $notification['id'], + $notification['tr_id'], + Util::numberFormat($notification['tr_amount']), + $notification['tr_crc'], + $this->merchantSecret, + $notification['md5sum'] + ); + } + + /** @return string */ + private function getResourcePrefix() + { + if ($this->productionMode) { + return self::PRODUCTION_PREFIX; + } + + return self::SANDBOX_PREFIX; + } + + private function getNotificationObject() + { + if (!isset($_POST['tr_id'])) { + throw new TException('Not recognised or invalid notification type. POST: '.json_encode($_POST)); + } + + return $this->getResponse(new PaymentTypeBasic()); + } +}