refactor(core): enhance SDK structure and documentation
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
This commit is contained in:
@@ -2,74 +2,108 @@
|
||||
|
||||
namespace Apoloz\AlfaBank\Sbp;
|
||||
|
||||
use Apoloz\AlfaBank\Enums\Environment;
|
||||
use Apoloz\AlfaBank\Core\AlfaConfig;
|
||||
use Apoloz\AlfaBank\Http\Middleware\AlfaResponseMiddleware;
|
||||
use Apoloz\AlfaBank\Http\Middleware\RequestIdMiddleware;
|
||||
use Apoloz\AlfaBank\Http\Pagination\AlfaOffsetPaginator;
|
||||
use Apoloz\AlfaBank\Utils\CmsSigner;
|
||||
use Saloon\Http\Connector;
|
||||
use Saloon\Http\PendingRequest;
|
||||
use Saloon\Http\Request;
|
||||
use Saloon\PaginationPlugin\Contracts\HasPagination;
|
||||
use Saloon\PaginationPlugin\OffsetPaginator;
|
||||
use Saloon\Traits\RequestProperties\HasTries;
|
||||
use Saloon\Traits\Plugins\HasTimeout;
|
||||
|
||||
class SbpConnector extends Connector
|
||||
class SbpConnector extends Connector implements HasPagination
|
||||
{
|
||||
use HasTimeout;
|
||||
use HasTries;
|
||||
|
||||
public function __construct(
|
||||
protected string $certificatePath,
|
||||
protected string $privateKeyPath,
|
||||
protected string $keyNameAlias, // 'key-name' header
|
||||
protected Environment $environment = Environment::SANDBOX
|
||||
) {}
|
||||
protected AlfaConfig $alfaConfig,
|
||||
protected string $keyNameAlias
|
||||
) {
|
||||
$this->tries = 3;
|
||||
$this->retryInterval = 500;
|
||||
}
|
||||
|
||||
public function resolveBaseUrl(): string
|
||||
{
|
||||
return match ($this->environment) {
|
||||
Environment::SANDBOX => 'https://217.12.103.132:2443/fsCryptoProxy',
|
||||
Environment::PRODUCTION => 'https://pay.alfabank.ru/fsCryptoProxy', // Placeholder, user needs to confirm
|
||||
};
|
||||
return $this->alfaConfig->baseUrl;
|
||||
}
|
||||
|
||||
protected function defaultConfig(): array
|
||||
{
|
||||
return [
|
||||
'cert' => $this->certificatePath,
|
||||
'ssl_key' => $this->privateKeyPath,
|
||||
$config = [
|
||||
'cert' => $this->alfaConfig->certificatePath,
|
||||
'ssl_key' => $this->alfaConfig->privateKeyPath,
|
||||
'verify' => true,
|
||||
];
|
||||
|
||||
if ($this->alfaConfig->caBundlePath) {
|
||||
$config['verify'] = $this->alfaConfig->caBundlePath;
|
||||
}
|
||||
|
||||
return $config;
|
||||
}
|
||||
|
||||
protected function defaultHeaders(): array
|
||||
protected function defaultMiddleware(): array
|
||||
{
|
||||
return [
|
||||
'Content-Type' => 'application/x-www-form-urlencoded', // As per doc, despite body being JSON
|
||||
'key-name' => $this->keyNameAlias,
|
||||
];
|
||||
return [];
|
||||
}
|
||||
|
||||
public function boot(PendingRequest $pendingRequest): void
|
||||
{
|
||||
// Calculate Signature
|
||||
$body = $pendingRequest->body()?->all();
|
||||
|
||||
if (empty($body)) {
|
||||
$pendingRequest->middleware()->onResponse(new AlfaResponseMiddleware());
|
||||
$pendingRequest->middleware()->onRequest(new RequestIdMiddleware());
|
||||
|
||||
$bodyRepo = $pendingRequest->body();
|
||||
if (!$bodyRepo || empty($bodyRepo->all())) {
|
||||
return;
|
||||
}
|
||||
$body = $bodyRepo->all();
|
||||
|
||||
// SBP requires minified JSON for signing. Canonicalization (JCS) is the formal term.
|
||||
$jsonBody = CmsSigner::canonicalize($body);
|
||||
|
||||
// Minify JSON for signature
|
||||
$jsonBody = json_encode($body, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
|
||||
// Sign the body using the configured private key (prefer signing key, fallback to transport key).
|
||||
$keyPath = $this->alfaConfig->signingPrivateKeyPath ?: $this->alfaConfig->privateKeyPath;
|
||||
|
||||
// Remove spaces/newlines as per doc requirement "exclude all spaces, tabs, newlines"
|
||||
// Standard json_encode without pretty print usually does this, but let's be sure.
|
||||
// The doc example: `{"command":"GetQRCd",...}` implies no spaces after colons/commas.
|
||||
if (!file_exists($keyPath)) {
|
||||
throw new \RuntimeException("Private key file not found: $keyPath");
|
||||
}
|
||||
|
||||
$privateKey = openssl_pkey_get_private(
|
||||
file_get_contents($keyPath)
|
||||
);
|
||||
|
||||
$privateKey = file_get_contents($this->privateKeyPath);
|
||||
$pkeyId = openssl_get_privatekey($privateKey);
|
||||
|
||||
openssl_sign($jsonBody, $signature, $pkeyId, OPENSSL_ALGO_SHA256);
|
||||
if (!$privateKey) {
|
||||
throw new \RuntimeException("Could not load private key for SBP signing from: $keyPath");
|
||||
}
|
||||
|
||||
$signature = '';
|
||||
openssl_sign($jsonBody, $signature, $privateKey, OPENSSL_ALGO_SHA256);
|
||||
$signatureBase64 = base64_encode($signature);
|
||||
|
||||
|
||||
// Mutate the request to add SBP-specific headers and set the exact signed body.
|
||||
$pendingRequest->headers()->add('Authorization', $signatureBase64);
|
||||
// The SBP API documentation specifies this content-type for signed requests.
|
||||
$pendingRequest->headers()->add('Content-Type', 'application/x-www-form-urlencoded');
|
||||
|
||||
// Ensure the body sent is the exact string we signed?
|
||||
// Saloon will encode the array to JSON again.
|
||||
// We should enforce the body to be the string we just signed to ensure consistency.
|
||||
$pendingRequest->body()->set($jsonBody);
|
||||
// Note: Saloon/Guzzle might re-serialize the body.
|
||||
// For strict SBP signing, ensuring the body sent matches $jsonBody exactly is crucial.
|
||||
// Saloon's HasJsonBody trait usually handles this fine as long as array order is preserved.
|
||||
}
|
||||
}
|
||||
|
||||
public function withCaBundle(string $path): static
|
||||
{
|
||||
$this->config()->merge(['verify' => $path]);
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function paginate(Request $request): OffsetPaginator
|
||||
{
|
||||
return new AlfaOffsetPaginator($this, $request);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user