Passed
Pull Request — master (#909)
by Songda
02:12
created

get_wechat_url()   A

Complexity

Conditions 5
Paths 7

Size

Total Lines 21
Code Lines 10

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 10
dl 0
loc 21
rs 9.6111
c 0
b 0
f 0
cc 5
nc 7
nop 2
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\Direction\NoHttpRequestDirection;
13
use Yansongda\Pay\Exception\ContainerException;
14
use Yansongda\Pay\Exception\DecryptException;
15
use Yansongda\Pay\Exception\Exception;
16
use Yansongda\Pay\Exception\InvalidConfigException;
17
use Yansongda\Pay\Exception\InvalidParamsException;
18
use Yansongda\Pay\Exception\InvalidSignException;
19
use Yansongda\Pay\Exception\ServiceNotFoundException;
20
use Yansongda\Pay\Plugin\ParserPlugin;
21
use Yansongda\Pay\Plugin\Wechat\AddPayloadBodyPlugin;
22
use Yansongda\Pay\Plugin\Wechat\AddPayloadSignaturePlugin;
23
use Yansongda\Pay\Plugin\Wechat\AddRadarPlugin;
24
use Yansongda\Pay\Plugin\Wechat\ResponsePlugin;
25
use Yansongda\Pay\Plugin\Wechat\StartPlugin;
26
use Yansongda\Pay\Plugin\Wechat\WechatPublicCertsPlugin;
27
use Yansongda\Pay\Provider\Wechat;
28
use Yansongda\Supports\Collection;
29
use Yansongda\Supports\Str;
30
31
function should_do_http_request(string $direction): bool
32
{
33
    return NoHttpRequestDirection::class !== $direction
34
        && !in_array(NoHttpRequestDirection::class, class_parents($direction));
35
}
36
37
function get_tenant(array $params = []): string
38
{
39
    return strval($params['_config'] ?? 'default');
40
}
41
42
/**
43
 * @throws InvalidConfigException
44
 */
45
function get_direction(mixed $direction): DirectionInterface
46
{
47
    try {
48
        $direction = Pay::get($direction);
49
50
        $direction = is_string($direction) ? Pay::get($direction) : $direction;
51
    } catch (ContainerException|ServiceNotFoundException) {
0 ignored issues
show
Coding Style Comprehensibility introduced by
Consider adding a comment why this CATCH block is empty.
Loading history...
52
    }
53
54
    if (!$direction instanceof DirectionInterface) {
55
        throw new InvalidConfigException(Exception::CONFIG_DIRECTION_INVALID, '配置异常: 配置的 DirectionInterface 未实现 `DirectionInterface`');
56
    }
57
58
    return $direction;
59
}
60
61
function get_public_cert(string $key): string
62
{
63
    return Str::endsWith($key, ['.cer', '.crt', '.pem']) ? file_get_contents($key) : $key;
64
}
65
66
function get_private_cert(string $key): string
67
{
68
    if (Str::endsWith($key, ['.crt', '.pem'])) {
69
        return file_get_contents($key);
70
    }
71
72
    return "-----BEGIN RSA PRIVATE KEY-----\n".
73
        wordwrap($key, 64, "\n", true).
74
        "\n-----END RSA PRIVATE KEY-----";
75
}
76
77
function filter_params(array $params, ?Closure $closure = null): array
78
{
79
    return array_filter($params, static fn ($v, $k) => !Str::startsWith($k, '_') && !is_null($v) && (empty($closure) || $closure($k, $v)), ARRAY_FILTER_USE_BOTH);
80
}
81
82
/**
83
 * @throws ContainerException
84
 * @throws ServiceNotFoundException
85
 */
86
function get_alipay_config(array $params = []): array
87
{
88
    $alipay = Pay::get(ConfigInterface::class)->get('alipay');
89
90
    return $alipay[get_tenant($params)] ?? [];
91
}
92
93
/**
94
 * @throws InvalidConfigException
95
 * @throws InvalidSignException
96
 */
97
function verify_alipay_sign(array $config, string $contents, string $sign): void
98
{
99
    $public = $config['alipay_public_cert_path'] ?? null;
100
101
    if (empty($public)) {
102
        throw new InvalidConfigException(Exception::CONFIG_ALIPAY_INVALID, '配置异常: 缺少支付宝配置 -- [alipay_public_cert_path]');
103
    }
104
105
    $result = 1 === openssl_verify(
106
        $contents,
107
        base64_decode($sign),
108
        get_public_cert($public),
109
        OPENSSL_ALGO_SHA256
110
    );
111
112
    if (!$result) {
113
        throw new InvalidSignException(Exception::SIGN_ERROR, '签名异常: 验证支付宝签名失败', func_get_args());
114
    }
115
}
116
117
/**
118
 * @throws ContainerException
119
 * @throws ServiceNotFoundException
120
 */
121
function get_wechat_config(array $params = []): array
122
{
123
    $wechat = Pay::get(ConfigInterface::class)->get('wechat');
124
125
    return $wechat[get_tenant($params)] ?? [];
126
}
127
128
function get_wechat_method(?Collection $payload): string
129
{
130
    return strtoupper($payload?->get('_method') ?? 'POST');
131
}
132
133
/**
134
 * @throws InvalidParamsException
135
 */
136
function get_wechat_url(array $config, ?Collection $payload): string
137
{
138
    $url = $payload?->get('_url') ?? null;
139
140
    if (Pay::MODE_SERVICE === ($config['mode'] ?? Pay::MODE_NORMAL)) {
141
        $url = $payload?->get('_service_url') ?? $url ?? null;
142
143
        if (empty($url)) {
144
            throw new InvalidParamsException(Exception::PARAMS_WECHAT_URL_MISSING, '参数异常: 微信服务商模式下 `_service_url` 参数缺失:你可能用错插件顺序,应该先使用 `业务插件` 或者此业务模式不支持服务商模式');
145
        }
146
    }
147
148
    if (empty($url)) {
149
        throw new InvalidParamsException(Exception::PARAMS_WECHAT_URL_MISSING, '参数异常: 微信 `_url` 参数缺失:你可能用错插件顺序,应该先使用 `业务插件` 或者此业务模式不支持普通商户模式');
150
    }
151
152
    if (str_starts_with($url, 'http')) {
153
        return $url;
154
    }
155
156
    return Wechat::URL[$config['mode'] ?? Pay::MODE_NORMAL].$url;
157
}
158
159
/**
160
 * @throws InvalidParamsException
161
 */
162
function get_wechat_body(?Collection $payload): string
163
{
164
    $body = $payload?->get('_body') ?? null;
165
166
    if (is_null($body)) {
167
        throw new InvalidParamsException(Exception::PARAMS_WECHAT_BODY_MISSING, '参数异常: 微信 `_body` 参数缺失:你可能用错插件顺序,应该先使用 `AddPayloadBodyPlugin`');
168
    }
169
170
    return $body;
171
}
172
173
function get_wechat_config_type_key(array $params): string
174
{
175
    $key = ($params['_type'] ?? 'mp').'_app_id';
176
177
    if ('app_app_id' === $key) {
178
        $key = 'app_id';
179
    }
180
181
    return $key;
182
}
183
184
/**
185
 * @throws InvalidConfigException
186
 */
187
function get_wechat_sign(array $config, string $contents): string
188
{
189
    $privateKey = $config['mch_secret_cert'] ?? null;
190
191
    if (empty($privateKey)) {
192
        throw new InvalidConfigException(Exception::CONFIG_WECHAT_INVALID, '配置异常: 缺少微信配置 -- [mch_secret_cert]');
193
    }
194
195
    $privateKey = get_private_cert($privateKey);
196
197
    openssl_sign($contents, $sign, $privateKey, 'sha256WithRSAEncryption');
198
199
    return base64_encode($sign);
200
}
201
202
/**
203
 * @throws ContainerException
204
 * @throws ServiceNotFoundException
205
 * @throws InvalidConfigException
206
 */
207
function get_wechat_sign_v2(array $params, array $payload, bool $upper = true): string
208
{
209
    $key = get_wechat_config($params)['mch_secret_key_v2'] ?? null;
210
211
    if (empty($key)) {
212
        throw new InvalidConfigException(Exception::CONFIG_WECHAT_INVALID, '配置异常: 缺少微信配置 -- [mch_secret_key_v2]');
213
    }
214
215
    ksort($payload);
216
217
    $buff = '';
218
219
    foreach ($payload as $k => $v) {
220
        $buff .= ('sign' != $k && '' != $v && !is_array($v)) ? $k.'='.$v.'&' : '';
221
    }
222
223
    $sign = md5($buff.'key='.$key);
224
225
    return $upper ? strtoupper($sign) : $sign;
226
}
227
228
/**
229
 * @throws ContainerException
230
 * @throws DecryptException
231
 * @throws InvalidConfigException
232
 * @throws InvalidParamsException
233
 * @throws InvalidSignException
234
 * @throws ServiceNotFoundException
235
 */
236
function verify_wechat_sign(ResponseInterface|ServerRequestInterface $message, array $params): void
237
{
238
    if ($message instanceof ServerRequestInterface && 'localhost' === $message->getUri()->getHost()) {
239
        return;
240
    }
241
242
    $wechatSerial = $message->getHeaderLine('Wechatpay-Serial');
243
    $timestamp = $message->getHeaderLine('Wechatpay-Timestamp');
244
    $random = $message->getHeaderLine('Wechatpay-Nonce');
245
    $sign = $message->getHeaderLine('Wechatpay-Signature');
246
    $body = (string) $message->getBody();
247
248
    $content = $timestamp."\n".$random."\n".$body."\n";
249
    $public = get_wechat_config($params)['wechat_public_cert_path'][$wechatSerial] ?? null;
250
251
    if (empty($sign)) {
252
        throw new InvalidSignException(Exception::SIGN_EMPTY, '签名异常: 微信签名为空', ['headers' => $message->getHeaders(), 'body' => $body]);
253
    }
254
255
    $public = get_public_cert(
256
        empty($public) ? reload_wechat_public_certs($params, $wechatSerial) : $public
257
    );
258
259
    $result = 1 === openssl_verify(
260
        $content,
261
        base64_decode($sign),
262
        $public,
263
        'sha256WithRSAEncryption'
264
    );
265
266
    if (!$result) {
267
        throw new InvalidSignException(Exception::SIGN_ERROR, '签名异常: 验证微信签名失败', ['headers' => $message->getHeaders(), 'body' => $body]);
268
    }
269
}
270
271
function encrypt_wechat_contents(string $contents, string $publicKey): ?string
272
{
273
    if (openssl_public_encrypt($contents, $encrypted, get_public_cert($publicKey), OPENSSL_PKCS1_OAEP_PADDING)) {
274
        return base64_encode($encrypted);
275
    }
276
277
    return null;
278
}
279
280
/**
281
 * @throws ContainerException
282
 * @throws DecryptException
283
 * @throws InvalidConfigException
284
 * @throws InvalidParamsException
285
 * @throws ServiceNotFoundException
286
 */
287
function reload_wechat_public_certs(array $params, ?string $serialNo = null): string
288
{
289
    $data = Pay::wechat()->pay(
290
        [StartPlugin::class, WechatPublicCertsPlugin::class, AddPayloadBodyPlugin::class, AddPayloadSignaturePlugin::class, AddRadarPlugin::class, ResponsePlugin::class, ParserPlugin::class],
291
        $params
292
    )->get('data', []);
0 ignored issues
show
Bug introduced by
The method get() does not exist on Psr\Http\Message\MessageInterface. It seems like you code against a sub-type of Psr\Http\Message\MessageInterface such as Yansongda\Pay\Request. ( Ignorable by Annotation )

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

292
    )->/** @scrutinizer ignore-call */ get('data', []);
Loading history...
293
294
    $wechatConfig = get_wechat_config($params);
295
296
    foreach ($data as $item) {
297
        $certs[$item['serial_no']] = decrypt_wechat_resource($item['encrypt_certificate'], $wechatConfig)['ciphertext'] ?? '';
298
    }
299
300
    Pay::get(ConfigInterface::class)->set(
301
        'wechat.'.get_tenant($params).'.wechat_public_cert_path',
302
        ((array) ($wechatConfig['wechat_public_cert_path'] ?? [])) + ($certs ?? []),
303
    );
304
305
    if (!is_null($serialNo) && empty($certs[$serialNo])) {
306
        throw new InvalidConfigException(Exception::CONFIG_WECHAT_INVALID, '配置异常: 获取微信 wechat_public_cert_path 配置失败');
307
    }
308
309
    return $certs[$serialNo] ?? '';
310
}
311
312
/**
313
 * @throws ContainerException
314
 * @throws DecryptException
315
 * @throws InvalidConfigException
316
 * @throws InvalidParamsException
317
 * @throws ServiceNotFoundException
318
 */
319
function get_wechat_public_certs(array $params = [], ?string $path = null): void
320
{
321
    reload_wechat_public_certs($params);
322
323
    $config = get_wechat_config($params);
324
325
    if (empty($path)) {
326
        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...
327
328
        return;
329
    }
330
331
    foreach ($config['wechat_public_cert_path'] as $serialNo => $cert) {
332
        file_put_contents($path.'/'.$serialNo.'.crt', $cert);
333
    }
334
}
335
336
/**
337
 * @throws InvalidConfigException
338
 * @throws DecryptException
339
 */
340
function decrypt_wechat_resource(array $resource, array $config): array
341
{
342
    $ciphertext = base64_decode($resource['ciphertext'] ?? '');
343
    $secret = $config['mch_secret_key'] ?? null;
344
345
    if (strlen($ciphertext) <= Wechat::AUTH_TAG_LENGTH_BYTE) {
346
        throw new DecryptException(Exception::DECRYPT_WECHAT_CIPHERTEXT_PARAMS_INVALID, '加解密异常: ciphertext 位数过短');
347
    }
348
349
    if (is_null($secret) || Wechat::MCH_SECRET_KEY_LENGTH_BYTE != strlen($secret)) {
350
        throw new InvalidConfigException(Exception::CONFIG_WECHAT_INVALID, '配置异常: 缺少微信配置 -- [mch_secret_key]');
351
    }
352
353
    $resource['ciphertext'] = match ($resource['algorithm'] ?? '') {
354
        'AEAD_AES_256_GCM' => decrypt_wechat_resource_aes_256_gcm($ciphertext, $secret, $resource['nonce'] ?? '', $resource['associated_data'] ?? ''),
355
        default => throw new DecryptException(Exception::DECRYPT_WECHAT_DECRYPTED_METHOD_INVALID, '加解密异常: algorithm 不支持'),
356
    };
357
358
    return $resource;
359
}
360
361
/**
362
 * @throws DecryptException
363
 */
364
function decrypt_wechat_resource_aes_256_gcm(string $ciphertext, string $secret, string $nonce, string $associatedData): array|string
365
{
366
    $decrypted = openssl_decrypt(
367
        substr($ciphertext, 0, -Wechat::AUTH_TAG_LENGTH_BYTE),
368
        'aes-256-gcm',
369
        $secret,
370
        OPENSSL_RAW_DATA,
371
        $nonce,
372
        substr($ciphertext, -Wechat::AUTH_TAG_LENGTH_BYTE),
373
        $associatedData
374
    );
375
376
    if (false === $decrypted) {
377
        throw new DecryptException(Exception::DECRYPT_WECHAT_ENCRYPTED_DATA_INVALID, '加解密异常: 解密失败,请检查微信 mch_secret_key 是否正确');
378
    }
379
380
    if ('certificate' !== $associatedData) {
381
        $decrypted = json_decode($decrypted, true);
382
383
        if (JSON_ERROR_NONE !== json_last_error()) {
384
            throw new DecryptException(Exception::DECRYPT_WECHAT_ENCRYPTED_DATA_INVALID, '加解密异常: 待解密数据非正常数据');
385
        }
386
    }
387
388
    return $decrypted;
389
}
390
391
/**
392
 * @throws ContainerException
393
 * @throws DecryptException
394
 * @throws InvalidConfigException
395
 * @throws InvalidParamsException
396
 * @throws ServiceNotFoundException
397
 */
398
function get_wechat_serial_no(array $params): string
399
{
400
    if (!empty($params['_serial_no'])) {
401
        return $params['_serial_no'];
402
    }
403
404
    $config = get_wechat_config($params);
405
406
    if (empty($config['wechat_public_cert_path'])) {
407
        reload_wechat_public_certs($params);
408
409
        $config = get_wechat_config($params);
410
    }
411
412
    mt_srand();
413
414
    return strval(array_rand($config['wechat_public_cert_path']));
415
}
416
417
/**
418
 * @throws InvalidParamsException
419
 */
420
function get_wechat_public_key(array $config, string $serialNo): string
421
{
422
    $publicKey = $config['wechat_public_cert_path'][$serialNo] ?? null;
423
424
    if (empty($publicKey)) {
425
        throw new InvalidParamsException(Exception::PARAMS_WECHAT_SERIAL_NOT_FOUND, '参数异常: 微信公钥序列号为找到 -'.$serialNo);
426
    }
427
428
    return $publicKey;
429
}
430
431
/**
432
 * @throws ContainerException
433
 * @throws ServiceNotFoundException
434
 */
435
function get_unipay_config(array $params): array
436
{
437
    $unipay = Pay::get(ConfigInterface::class)->get('unipay');
438
439
    return $unipay[get_tenant($params)] ?? [];
440
}
441
442
/**
443
 * @throws ContainerException
444
 * @throws InvalidConfigException
445
 * @throws ServiceNotFoundException
446
 * @throws InvalidSignException
447
 */
448
function verify_unipay_sign(array $params, string $contents, string $sign): void
449
{
450
    if (empty($params['signPubKeyCert'])
451
        && empty($public = get_unipay_config($params)['unipay_public_cert_path'] ?? null)) {
452
        throw new InvalidConfigException(Exception::CONFIG_UNIPAY_INVALID, '配置异常: 缺少银联配置 -- [unipay_public_cert_path]');
453
    }
454
455
    $result = 1 === openssl_verify(
456
        hash('sha256', $contents),
457
        base64_decode($sign),
458
        get_public_cert($params['signPubKeyCert'] ?? $public ?? ''),
459
        'sha256'
460
    );
461
462
    if (!$result) {
463
        throw new InvalidSignException(Exception::SIGN_ERROR, '签名异常: 验证银联签名失败', func_get_args());
464
    }
465
}
466