Passed
Push — master ( c57d84...4857dc )
by Songda
02:27 queued 27s
created

get_alipay_url()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 9
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 2
eloc 4
c 0
b 0
f 0
nc 2
nop 2
dl 0
loc 9
rs 10
1
<?php
2
3
declare(strict_types=1);
4
5
namespace Yansongda\Pay;
6
7
use Closure;
8
use Psr\Http\Message\ResponseInterface;
9
use Psr\Http\Message\ServerRequestInterface;
10
use Yansongda\Pay\Contract\ConfigInterface;
11
use Yansongda\Pay\Contract\DirectionInterface;
12
use Yansongda\Pay\Contract\PackerInterface;
13
use Yansongda\Pay\Direction\NoHttpRequestDirection;
14
use Yansongda\Pay\Exception\ContainerException;
15
use Yansongda\Pay\Exception\DecryptException;
16
use Yansongda\Pay\Exception\Exception;
17
use Yansongda\Pay\Exception\InvalidConfigException;
18
use Yansongda\Pay\Exception\InvalidParamsException;
19
use Yansongda\Pay\Exception\InvalidSignException;
20
use Yansongda\Pay\Exception\ServiceNotFoundException;
21
use Yansongda\Pay\Plugin\ParserPlugin;
22
use Yansongda\Pay\Plugin\Wechat\AddRadarPlugin;
23
use Yansongda\Pay\Plugin\Wechat\ResponsePlugin;
24
use Yansongda\Pay\Plugin\Wechat\StartPlugin;
25
use Yansongda\Pay\Plugin\Wechat\V3\AddPayloadBodyPlugin;
26
use Yansongda\Pay\Plugin\Wechat\V3\AddPayloadSignaturePlugin;
27
use Yansongda\Pay\Plugin\Wechat\V3\WechatPublicCertsPlugin;
28
use Yansongda\Pay\Provider\Alipay;
29
use Yansongda\Pay\Provider\Unipay;
30
use Yansongda\Pay\Provider\Wechat;
31
use Yansongda\Supports\Collection;
32
use Yansongda\Supports\Str;
33
34
function should_do_http_request(string $direction): bool
35
{
36
    return NoHttpRequestDirection::class !== $direction
37
        && !in_array(NoHttpRequestDirection::class, class_parents($direction));
38
}
39
40
function get_tenant(array $params = []): string
41
{
42
    return strval($params['_config'] ?? 'default');
43
}
44
45
/**
46
 * @throws InvalidConfigException
47
 */
48
function get_direction(mixed $direction): DirectionInterface
49
{
50
    try {
51
        $direction = Pay::get($direction);
52
53
        $direction = is_string($direction) ? Pay::get($direction) : $direction;
54
    } catch (ContainerException|ServiceNotFoundException) {
0 ignored issues
show
Coding Style Comprehensibility introduced by
Consider adding a comment why this CATCH block is empty.
Loading history...
55
    }
56
57
    if (!$direction instanceof DirectionInterface) {
58
        throw new InvalidConfigException(Exception::CONFIG_DIRECTION_INVALID, '配置异常: 配置的 DirectionInterface 未实现 `DirectionInterface`');
59
    }
60
61
    return $direction;
62
}
63
64
/**
65
 * @throws InvalidConfigException
66
 */
67
function get_packer(mixed $packer): PackerInterface
68
{
69
    try {
70
        $packer = Pay::get($packer);
71
72
        $packer = is_string($packer) ? Pay::get($packer) : $packer;
73
    } catch (ContainerException|ServiceNotFoundException) {
0 ignored issues
show
Coding Style Comprehensibility introduced by
Consider adding a comment why this CATCH block is empty.
Loading history...
74
    }
75
76
    if (!$packer instanceof PackerInterface) {
77
        throw new InvalidConfigException(Exception::CONFIG_PACKER_INVALID, '配置参数异常: 配置的 `PackerInterface` 未实现 `PackerInterface`');
78
    }
79
80
    return $packer;
81
}
82
83
function get_public_cert(string $key): string
84
{
85
    return Str::endsWith($key, ['.cer', '.crt', '.pem']) ? file_get_contents($key) : $key;
86
}
87
88
function get_private_cert(string $key): string
89
{
90
    if (Str::endsWith($key, ['.crt', '.pem'])) {
91
        return file_get_contents($key);
92
    }
93
94
    return "-----BEGIN RSA PRIVATE KEY-----\n".
95
        wordwrap($key, 64, "\n", true).
96
        "\n-----END RSA PRIVATE KEY-----";
97
}
98
99
function filter_params(null|array|Collection $params, ?Closure $closure = null): array
100
{
101
    $params = Collection::wrap($params);
102
103
    return $params->filter(static fn ($v, $k) => !Str::startsWith($k, '_') && !is_null($v) && (empty($closure) || $closure($k, $v)))->toArray();
104
}
105
106
function get_radar_method(?Collection $payload): ?string
107
{
108
    $string = $payload?->get('_method') ?? null;
109
110
    if (is_null($string)) {
111
        return null;
112
    }
113
114
    return strtoupper($string);
115
}
116
117
function get_radar_url(array $config, ?Collection $payload): ?string
118
{
119
    return match ($config['mode'] ?? Pay::MODE_NORMAL) {
120
        Pay::MODE_SERVICE => $payload?->get('_service_url') ?? $payload?->get('_url') ?? null,
121
        Pay::MODE_SANDBOX => $payload?->get('_sandbox_url') ?? $payload?->get('_url') ?? null,
122
        default => $payload?->get('_url') ?? null,
123
    };
124
}
125
126
function get_radar_body(?Collection $payload): ?string
127
{
128
    return $payload?->get('_body') ?? null;
129
}
130
131
/**
132
 * @throws ContainerException
133
 * @throws ServiceNotFoundException
134
 */
135
function get_alipay_config(array $params = []): array
136
{
137
    $alipay = Pay::get(ConfigInterface::class)->get('alipay');
138
139
    return $alipay[get_tenant($params)] ?? [];
140
}
141
142
function get_alipay_url(array $config, ?Collection $payload): string
143
{
144
    $url = get_radar_url($config, $payload) ?? '';
145
146
    if (str_starts_with($url, 'http')) {
147
        return $url;
148
    }
149
150
    return Alipay::URL[$config['mode'] ?? Pay::MODE_NORMAL];
151
}
152
153
/**
154
 * @throws InvalidConfigException
155
 * @throws InvalidSignException
156
 */
157
function verify_alipay_sign(array $config, string $contents, string $sign): void
158
{
159
    if (empty($sign)) {
160
        throw new InvalidSignException(Exception::SIGN_EMPTY, '签名异常: 验证支付宝签名失败-支付宝签名为空', func_get_args());
161
    }
162
163
    $public = $config['alipay_public_cert_path'] ?? null;
164
165
    if (empty($public)) {
166
        throw new InvalidConfigException(Exception::CONFIG_ALIPAY_INVALID, '配置异常: 缺少支付宝配置 -- [alipay_public_cert_path]');
167
    }
168
169
    $result = 1 === openssl_verify(
170
        $contents,
171
        base64_decode($sign),
172
        get_public_cert($public),
173
        OPENSSL_ALGO_SHA256
174
    );
175
176
    if (!$result) {
177
        throw new InvalidSignException(Exception::SIGN_ERROR, '签名异常: 验证支付宝签名失败', func_get_args());
178
    }
179
}
180
181
/**
182
 * @throws ContainerException
183
 * @throws ServiceNotFoundException
184
 */
185
function get_wechat_config(array $params = []): array
186
{
187
    $wechat = Pay::get(ConfigInterface::class)->get('wechat');
188
189
    return $wechat[get_tenant($params)] ?? [];
190
}
191
192
function get_wechat_method(?Collection $payload): string
193
{
194
    return get_radar_method($payload) ?? 'POST';
195
}
196
197
/**
198
 * @throws InvalidParamsException
199
 */
200
function get_wechat_url(array $config, ?Collection $payload): string
201
{
202
    $url = get_radar_url($config, $payload);
203
204
    if (empty($url)) {
205
        throw new InvalidParamsException(Exception::PARAMS_WECHAT_URL_MISSING, '参数异常: 微信 `_url` 或 `_service_url` 参数缺失:你可能用错插件顺序,应该先使用 `业务插件`');
206
    }
207
208
    if (str_starts_with($url, 'http')) {
209
        return $url;
210
    }
211
212
    return Wechat::URL[$config['mode'] ?? Pay::MODE_NORMAL].$url;
213
}
214
215
/**
216
 * @throws InvalidParamsException
217
 */
218
function get_wechat_body(?Collection $payload): string
219
{
220
    $body = get_radar_body($payload);
221
222
    if (is_null($body)) {
223
        throw new InvalidParamsException(Exception::PARAMS_WECHAT_BODY_MISSING, '参数异常: 微信 `_body` 参数缺失:你可能用错插件顺序,应该先使用 `AddPayloadBodyPlugin`');
224
    }
225
226
    return $body;
227
}
228
229
function get_wechat_type_key(array $params): string
230
{
231
    $key = ($params['_type'] ?? 'mp').'_app_id';
232
233
    if ('app_app_id' === $key) {
234
        $key = 'app_id';
235
    }
236
237
    return $key;
238
}
239
240
/**
241
 * @throws InvalidConfigException
242
 */
243
function get_wechat_sign(array $config, string $contents): string
244
{
245
    $privateKey = $config['mch_secret_cert'] ?? null;
246
247
    if (empty($privateKey)) {
248
        throw new InvalidConfigException(Exception::CONFIG_WECHAT_INVALID, '配置异常: 缺少微信配置 -- [mch_secret_cert]');
249
    }
250
251
    $privateKey = get_private_cert($privateKey);
252
253
    openssl_sign($contents, $sign, $privateKey, 'sha256WithRSAEncryption');
254
255
    return base64_encode($sign);
256
}
257
258
/**
259
 * @throws InvalidConfigException
260
 */
261
function get_wechat_sign_v2(array $config, array $payload, bool $upper = true): string
262
{
263
    $key = $config['mch_secret_key_v2'] ?? null;
264
265
    if (empty($key)) {
266
        throw new InvalidConfigException(Exception::CONFIG_WECHAT_INVALID, '配置异常: 缺少微信配置 -- [mch_secret_key_v2]');
267
    }
268
269
    ksort($payload);
270
271
    $buff = '';
272
273
    foreach ($payload as $k => $v) {
274
        $buff .= ('sign' != $k && '' != $v && !is_array($v)) ? $k.'='.$v.'&' : '';
275
    }
276
277
    $sign = md5($buff.'key='.$key);
278
279
    return $upper ? strtoupper($sign) : $sign;
280
}
281
282
/**
283
 * @throws ContainerException
284
 * @throws DecryptException
285
 * @throws InvalidConfigException
286
 * @throws InvalidParamsException
287
 * @throws InvalidSignException
288
 * @throws ServiceNotFoundException
289
 */
290
function verify_wechat_sign(ResponseInterface|ServerRequestInterface $message, array $params): void
291
{
292
    if ($message instanceof ServerRequestInterface && 'localhost' === $message->getUri()->getHost()) {
293
        return;
294
    }
295
296
    $wechatSerial = $message->getHeaderLine('Wechatpay-Serial');
297
    $timestamp = $message->getHeaderLine('Wechatpay-Timestamp');
298
    $random = $message->getHeaderLine('Wechatpay-Nonce');
299
    $sign = $message->getHeaderLine('Wechatpay-Signature');
300
    $body = (string) $message->getBody();
301
302
    $content = $timestamp."\n".$random."\n".$body."\n";
303
    $public = get_wechat_config($params)['wechat_public_cert_path'][$wechatSerial] ?? null;
304
305
    if (empty($sign)) {
306
        throw new InvalidSignException(Exception::SIGN_EMPTY, '签名异常: 微信签名为空', ['headers' => $message->getHeaders(), 'body' => $body]);
307
    }
308
309
    $public = get_public_cert(
310
        empty($public) ? reload_wechat_public_certs($params, $wechatSerial) : $public
311
    );
312
313
    $result = 1 === openssl_verify(
314
        $content,
315
        base64_decode($sign),
316
        $public,
317
        'sha256WithRSAEncryption'
318
    );
319
320
    if (!$result) {
321
        throw new InvalidSignException(Exception::SIGN_ERROR, '签名异常: 验证微信签名失败', ['headers' => $message->getHeaders(), 'body' => $body]);
322
    }
323
}
324
325
/**
326
 * @throws InvalidConfigException
327
 * @throws InvalidSignException
328
 */
329
function verify_wechat_sign_v2(array $config, array $destination): void
330
{
331
    $sign = $destination['sign'] ?? null;
332
333
    if (empty($sign)) {
334
        throw new InvalidSignException(Exception::SIGN_EMPTY, '签名异常: 微信签名为空', $destination);
335
    }
336
337
    $key = $config['mch_secret_key_v2'] ?? null;
338
339
    if (empty($key)) {
340
        throw new InvalidConfigException(Exception::CONFIG_WECHAT_INVALID, '配置异常: 缺少微信配置 -- [mch_secret_key_v2]');
341
    }
342
343
    if (get_wechat_sign_v2($config, $destination) !== $sign) {
344
        throw new InvalidSignException(Exception::SIGN_ERROR, '签名异常: 验证微信签名失败', $destination);
345
    }
346
}
347
348
function encrypt_wechat_contents(string $contents, string $publicKey): ?string
349
{
350
    if (openssl_public_encrypt($contents, $encrypted, get_public_cert($publicKey), OPENSSL_PKCS1_OAEP_PADDING)) {
351
        return base64_encode($encrypted);
352
    }
353
354
    return null;
355
}
356
357
function decrypt_wechat_contents(string $encrypted, array $config): ?string
358
{
359
    if (openssl_private_decrypt(base64_decode($encrypted), $decrypted, get_private_cert($config['mch_secret_cert'] ?? ''), OPENSSL_PKCS1_OAEP_PADDING)) {
360
        return $decrypted;
361
    }
362
363
    return null;
364
}
365
366
/**
367
 * @throws ContainerException
368
 * @throws DecryptException
369
 * @throws InvalidConfigException
370
 * @throws InvalidParamsException
371
 * @throws ServiceNotFoundException
372
 */
373
function reload_wechat_public_certs(array $params, ?string $serialNo = null): string
374
{
375
    $data = Pay::wechat()->pay(
376
        [StartPlugin::class, WechatPublicCertsPlugin::class, AddPayloadBodyPlugin::class, AddPayloadSignaturePlugin::class, AddRadarPlugin::class, ResponsePlugin::class, ParserPlugin::class],
377
        $params
378
    )->get('data', []);
0 ignored issues
show
Bug introduced by
The method get() does not exist on Psr\Http\Message\MessageInterface. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

378
    )->/** @scrutinizer ignore-call */ get('data', []);

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
379
380
    $wechatConfig = get_wechat_config($params);
381
382
    foreach ($data as $item) {
383
        $certs[$item['serial_no']] = decrypt_wechat_resource($item['encrypt_certificate'], $wechatConfig)['ciphertext'] ?? '';
384
    }
385
386
    Pay::get(ConfigInterface::class)->set(
387
        'wechat.'.get_tenant($params).'.wechat_public_cert_path',
388
        ((array) ($wechatConfig['wechat_public_cert_path'] ?? [])) + ($certs ?? []),
389
    );
390
391
    if (!is_null($serialNo) && empty($certs[$serialNo])) {
392
        throw new InvalidConfigException(Exception::CONFIG_WECHAT_INVALID, '配置异常: 获取微信 wechat_public_cert_path 配置失败');
393
    }
394
395
    return $certs[$serialNo] ?? '';
396
}
397
398
/**
399
 * @throws ContainerException
400
 * @throws DecryptException
401
 * @throws InvalidConfigException
402
 * @throws InvalidParamsException
403
 * @throws ServiceNotFoundException
404
 */
405
function get_wechat_public_certs(array $params = [], ?string $path = null): void
406
{
407
    reload_wechat_public_certs($params);
408
409
    $config = get_wechat_config($params);
410
411
    if (empty($path)) {
412
        var_dump($config['wechat_public_cert_path']);
0 ignored issues
show
Security Debugging Code introduced by
var_dump($config['wechat_public_cert_path']) looks like debug code. Are you sure you do not want to remove it?
Loading history...
413
414
        return;
415
    }
416
417
    foreach ($config['wechat_public_cert_path'] as $serialNo => $cert) {
418
        file_put_contents($path.'/'.$serialNo.'.crt', $cert);
419
    }
420
}
421
422
/**
423
 * @throws InvalidConfigException
424
 * @throws DecryptException
425
 */
426
function decrypt_wechat_resource(array $resource, array $config): array
427
{
428
    $ciphertext = base64_decode($resource['ciphertext'] ?? '');
429
    $secret = $config['mch_secret_key'] ?? null;
430
431
    if (strlen($ciphertext) <= Wechat::AUTH_TAG_LENGTH_BYTE) {
432
        throw new DecryptException(Exception::DECRYPT_WECHAT_CIPHERTEXT_PARAMS_INVALID, '加解密异常: ciphertext 位数过短');
433
    }
434
435
    if (is_null($secret) || Wechat::MCH_SECRET_KEY_LENGTH_BYTE != strlen($secret)) {
436
        throw new InvalidConfigException(Exception::CONFIG_WECHAT_INVALID, '配置异常: 缺少微信配置 -- [mch_secret_key]');
437
    }
438
439
    $resource['ciphertext'] = match ($resource['algorithm'] ?? '') {
440
        'AEAD_AES_256_GCM' => decrypt_wechat_resource_aes_256_gcm($ciphertext, $secret, $resource['nonce'] ?? '', $resource['associated_data'] ?? ''),
441
        default => throw new DecryptException(Exception::DECRYPT_WECHAT_DECRYPTED_METHOD_INVALID, '加解密异常: algorithm 不支持'),
442
    };
443
444
    return $resource;
445
}
446
447
/**
448
 * @throws DecryptException
449
 */
450
function decrypt_wechat_resource_aes_256_gcm(string $ciphertext, string $secret, string $nonce, string $associatedData): array|string
451
{
452
    $decrypted = openssl_decrypt(
453
        substr($ciphertext, 0, -Wechat::AUTH_TAG_LENGTH_BYTE),
454
        'aes-256-gcm',
455
        $secret,
456
        OPENSSL_RAW_DATA,
457
        $nonce,
458
        substr($ciphertext, -Wechat::AUTH_TAG_LENGTH_BYTE),
459
        $associatedData
460
    );
461
462
    if (false === $decrypted) {
463
        throw new DecryptException(Exception::DECRYPT_WECHAT_ENCRYPTED_DATA_INVALID, '加解密异常: 解密失败,请检查微信 mch_secret_key 是否正确');
464
    }
465
466
    if ('certificate' !== $associatedData) {
467
        $decrypted = json_decode($decrypted, true);
468
469
        if (JSON_ERROR_NONE !== json_last_error()) {
470
            throw new DecryptException(Exception::DECRYPT_WECHAT_ENCRYPTED_DATA_INVALID, '加解密异常: 待解密数据非正常数据');
471
        }
472
    }
473
474
    return $decrypted;
475
}
476
477
/**
478
 * @throws ContainerException
479
 * @throws DecryptException
480
 * @throws InvalidConfigException
481
 * @throws InvalidParamsException
482
 * @throws ServiceNotFoundException
483
 */
484
function get_wechat_serial_no(array $params): string
485
{
486
    if (!empty($params['_serial_no'])) {
487
        return $params['_serial_no'];
488
    }
489
490
    $config = get_wechat_config($params);
491
492
    if (empty($config['wechat_public_cert_path'])) {
493
        reload_wechat_public_certs($params);
494
495
        $config = get_wechat_config($params);
496
    }
497
498
    mt_srand();
499
500
    return strval(array_rand($config['wechat_public_cert_path']));
501
}
502
503
/**
504
 * @throws InvalidParamsException
505
 */
506
function get_wechat_public_key(array $config, string $serialNo): string
507
{
508
    $publicKey = $config['wechat_public_cert_path'][$serialNo] ?? null;
509
510
    if (empty($publicKey)) {
511
        throw new InvalidParamsException(Exception::PARAMS_WECHAT_SERIAL_NOT_FOUND, '参数异常: 微信公钥序列号为找到 -'.$serialNo);
512
    }
513
514
    return $publicKey;
515
}
516
517
/**
518
 * @throws ContainerException
519
 * @throws ServiceNotFoundException
520
 */
521
function get_unipay_config(array $params = []): array
522
{
523
    $unipay = Pay::get(ConfigInterface::class)->get('unipay');
524
525
    return $unipay[get_tenant($params)] ?? [];
526
}
527
528
/**
529
 * @throws InvalidConfigException
530
 * @throws InvalidSignException
531
 */
532
function verify_unipay_sign(array $config, string $contents, string $sign, ?string $signPublicKeyCert = null): void
533
{
534
    if (empty($sign)) {
535
        throw new InvalidSignException(Exception::SIGN_EMPTY, '签名异常: 银联签名为空', func_get_args());
536
    }
537
538
    if (empty($signPublicKeyCert) && empty($public = $config['unipay_public_cert_path'] ?? null)) {
539
        throw new InvalidConfigException(Exception::CONFIG_UNIPAY_INVALID, '配置异常: 缺少银联配置 -- [unipay_public_cert_path]');
540
    }
541
542
    $result = 1 === openssl_verify(
543
        hash('sha256', $contents),
544
        base64_decode($sign),
545
        get_public_cert($signPublicKeyCert ?? $public ?? ''),
546
        'sha256'
547
    );
548
549
    if (!$result) {
550
        throw new InvalidSignException(Exception::SIGN_ERROR, '签名异常: 验证银联签名失败', func_get_args());
551
    }
552
}
553
554
/**
555
 * @throws InvalidParamsException
556
 */
557
function get_unipay_url(array $config, ?Collection $payload): string
558
{
559
    $url = get_radar_url($config, $payload);
560
561
    if (empty($url)) {
562
        throw new InvalidParamsException(Exception::PARAMS_UNIPAY_URL_MISSING, '参数异常: 银联 `_url` 参数缺失:你可能用错插件顺序,应该先使用 `业务插件`');
563
    }
564
565
    if (str_starts_with($url, 'http')) {
566
        return $url;
567
    }
568
569
    return Unipay::URL[$config['mode'] ?? Pay::MODE_NORMAL].$url;
570
}
571
572
/**
573
 * @throws InvalidParamsException
574
 */
575
function get_unipay_body(?Collection $payload): string
576
{
577
    $body = get_radar_body($payload);
578
579
    if (is_null($body)) {
580
        throw new InvalidParamsException(Exception::PARAMS_UNIPAY_BODY_MISSING, '参数异常: 银联 `_body` 参数缺失:你可能用错插件顺序,应该先使用 `AddPayloadBodyPlugin`');
581
    }
582
583
    return $body;
584
}
585
586
/**
587
 * @throws InvalidConfigException
588
 */
589
function get_unipay_sign_qra(array $config, array $payload): string
590
{
591
    $key = $config['mch_secret_key'] ?? null;
592
593
    if (empty($key)) {
594
        throw new InvalidConfigException(Exception::CONFIG_UNIPAY_INVALID, '配置异常: 缺少银联配置 -- [mch_secret_key]');
595
    }
596
597
    ksort($payload);
598
599
    $buff = '';
600
601
    foreach ($payload as $k => $v) {
602
        $buff .= ('sign' != $k && '' != $v && !is_array($v)) ? $k.'='.$v.'&' : '';
603
    }
604
605
    return strtoupper(md5($buff.'key='.$key));
606
}
607
608
/**
609
 * @throws InvalidConfigException
610
 * @throws InvalidSignException
611
 */
612
function verify_unipay_sign_qra(array $config, array $destination): void
613
{
614
    $sign = $destination['sign'] ?? null;
615
616
    if (empty($sign)) {
617
        throw new InvalidSignException(Exception::SIGN_EMPTY, '签名异常: 银联签名为空', $destination);
618
    }
619
620
    $key = $config['mch_secret_key'] ?? null;
621
622
    if (empty($key)) {
623
        throw new InvalidConfigException(Exception::CONFIG_UNIPAY_INVALID, '配置异常: 缺少银联配置 -- [mch_secret_key]');
624
    }
625
626
    if (get_unipay_sign_qra($config, $destination) !== $sign) {
627
        throw new InvalidSignException(Exception::SIGN_ERROR, '签名异常: 验证银联签名失败', $destination);
628
    }
629
}
630