123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235 |
- #!/usr/bin/env php
- <?php declare(strict_types=1);
- // load autoload.php
- $possibleFiles = [__DIR__.'/../vendor/autoload.php', __DIR__.'/../../../autoload.php', __DIR__.'/../../autoload.php'];
- $file = null;
- foreach ($possibleFiles as $possibleFile) {
- if (\file_exists($possibleFile)) {
- $file = $possibleFile;
- break;
- }
- }
- if (null === $file) {
- throw new \RuntimeException('Unable to locate autoload.php file.');
- }
- require_once $file;
- unset($possibleFiles, $possibleFile, $file);
- use GuzzleHttp\Middleware;
- use GuzzleHttp\Utils;
- use GuzzleHttp\Exception\RequestException;
- use Psr\Http\Message\ResponseInterface;
- use WeChatPay\Builder;
- use WeChatPay\ClientDecoratorInterface;
- use WeChatPay\Crypto\AesGcm;
- /**
- * CertificateDownloader class
- */
- class CertificateDownloader
- {
- private const DEFAULT_BASE_URI = 'https://api.mch.weixin.qq.com/';
- public function run(): void
- {
- $opts = $this->parseOpts();
- if (!$opts || isset($opts['help'])) {
- $this->printHelp();
- return;
- }
- if (isset($opts['version'])) {
- static::prompt(ClientDecoratorInterface::VERSION);
- return;
- }
- $this->job($opts);
- }
- /**
- * Before `verifier` executing, decrypt and put the platform certificate(s) into the `$certs` reference.
- *
- * @param string $apiv3Key
- * @param array<string,?string> $certs
- *
- * @return callable(ResponseInterface)
- */
- private static function certsInjector(string $apiv3Key, array &$certs): callable {
- return static function(ResponseInterface $response) use ($apiv3Key, &$certs): ResponseInterface {
- $body = (string) $response->getBody();
- /** @var object{data:array<object{encrypt_certificate:object{serial_no:string,nonce:string,associated_data:string}}>} $json */
- $json = Utils::jsonDecode($body);
- $data = \is_object($json) && isset($json->data) && \is_array($json->data) ? $json->data : [];
- \array_map(static function($row) use ($apiv3Key, &$certs) {
- $cert = $row->encrypt_certificate;
- $certs[$row->serial_no] = AesGcm::decrypt($cert->ciphertext, $apiv3Key, $cert->nonce, $cert->associated_data);
- }, $data);
- return $response;
- };
- }
- /**
- * @param array<string,string|true> $opts
- *
- * @return void
- */
- private function job(array $opts): void
- {
- static $certs = ['any' => null];
- $outputDir = $opts['output'] ?? \sys_get_temp_dir();
- $apiv3Key = (string) $opts['key'];
- $instance = Builder::factory([
- 'mchid' => $opts['mchid'],
- 'serial' => $opts['serialno'],
- 'privateKey' => \file_get_contents((string)$opts['privatekey']),
- 'certs' => &$certs,
- 'base_uri' => (string)($opts['baseuri'] ?? self::DEFAULT_BASE_URI),
- ]);
- /** @var \GuzzleHttp\HandlerStack $stack */
- $stack = $instance->getDriver()->select(ClientDecoratorInterface::JSON_BASED)->getConfig('handler');
- // The response middle stacks were executed one by one on `FILO` order.
- $stack->after('verifier', Middleware::mapResponse(static::certsInjector($apiv3Key, $certs)), 'injector');
- $stack->before('verifier', Middleware::mapResponse(static::certsRecorder((string) $outputDir, $certs)), 'recorder');
- $instance->chain('v3/certificates')->getAsync(
- ['debug' => true]
- )->otherwise(static function($exception) {
- static::prompt($exception->getMessage());
- if ($exception instanceof RequestException && $exception->hasResponse()) {
- /** @var ResponseInterface $response */
- $response = $exception->getResponse();
- static::prompt((string) $response->getBody(), '', '');
- }
- static::prompt($exception->getTraceAsString());
- })->wait();
- }
- /**
- * After `verifier` executed, wrote the platform certificate(s) onto disk.
- *
- * @param string $outputDir
- * @param array<string,?string> $certs
- *
- * @return callable(ResponseInterface)
- */
- private static function certsRecorder(string $outputDir, array &$certs): callable {
- return static function(ResponseInterface $response) use ($outputDir, &$certs): ResponseInterface {
- $body = (string) $response->getBody();
- /** @var object{data:array<object{effective_time:string,expire_time:string:serial_no:string}>} $json */
- $json = Utils::jsonDecode($body);
- $data = \is_object($json) && isset($json->data) && \is_array($json->data) ? $json->data : [];
- \array_walk($data, static function($row, $index, $certs) use ($outputDir) {
- $serialNo = $row->serial_no;
- $outpath = $outputDir . \DIRECTORY_SEPARATOR . 'wechatpay_' . $serialNo . '.pem';
- static::prompt(
- 'Certificate #' . $index . ' {',
- ' Serial Number: ' . static::highlight($serialNo),
- ' Not Before: ' . (new \DateTime($row->effective_time))->format(\DateTime::W3C),
- ' Not After: ' . (new \DateTime($row->expire_time))->format(\DateTime::W3C),
- ' Saved to: ' . static::highlight($outpath),
- ' You may confirm the above infos again even if this library already did(by Crypto\Rsa::verify):',
- ' ' . static::highlight(\sprintf('openssl x509 -in %s -noout -serial -dates', $outpath)),
- ' Content: ', '', $certs[$serialNo], '',
- '}'
- );
- \file_put_contents($outpath, $certs[$serialNo]);
- }, $certs);
- return $response;
- };
- }
- /**
- * @param string $thing
- */
- private static function highlight(string $thing): string
- {
- return \sprintf("\x1B[1;32m%s\x1B[0m", $thing);
- }
- /**
- * @param string $messages
- */
- private static function prompt(...$messages): void
- {
- \array_walk($messages, static function (string $message): void { \printf('%s%s', $message, \PHP_EOL); });
- }
- /**
- * @return ?array<string,string|true>
- */
- private function parseOpts(): ?array
- {
- $opts = [
- [ 'key', 'k', true ],
- [ 'mchid', 'm', true ],
- [ 'privatekey', 'f', true ],
- [ 'serialno', 's', true ],
- [ 'output', 'o', false ],
- // baseuri can be one of 'https://api2.mch.weixin.qq.com/', 'https://apihk.mch.weixin.qq.com/'
- [ 'baseuri', 'u', false ],
- ];
- $shortopts = 'hV';
- $longopts = [ 'help', 'version' ];
- foreach ($opts as $opt) {
- [$key, $alias] = $opt;
- $shortopts .= $alias . ':';
- $longopts[] = $key . ':';
- }
- $parsed = \getopt($shortopts, $longopts);
- if (!$parsed) {
- return null;
- }
- $args = [];
- foreach ($opts as $opt) {
- [$key, $alias, $mandatory] = $opt;
- if (isset($parsed[$key]) || isset($parsed[$alias])) {
- $possiable = $parsed[$key] ?? $parsed[$alias] ?? '';
- $args[$key] = (string) (\is_array($possiable) ? $possiable[0] : $possiable);
- } elseif ($mandatory) {
- return null;
- }
- }
- if (isset($parsed['h']) || isset($parsed['help'])) {
- $args['help'] = true;
- }
- if (isset($parsed['V']) || isset($parsed['version'])) {
- $args['version'] = true;
- }
- return $args;
- }
- private function printHelp(): void
- {
- static::prompt(
- 'Usage: 微信支付平台证书下载工具 [-hV]',
- ' -f=<privateKeyFilePath> -k=<apiv3Key> -m=<merchantId>',
- ' -s=<serialNo> -o=[outputFilePath] -u=[baseUri]',
- 'Options:',
- ' -m, --mchid=<merchantId> 商户号',
- ' -s, --serialno=<serialNo> 商户证书的序列号',
- ' -f, --privatekey=<privateKeyFilePath>',
- ' 商户的私钥文件',
- ' -k, --key=<apiv3Key> APIv3密钥',
- ' -o, --output=[outputFilePath]',
- ' 下载成功后保存证书的路径,可选,默认为临时文件目录夹',
- ' -u, --baseuri=[baseUri] 接入点,可选,默认为 ' . self::DEFAULT_BASE_URI,
- ' -V, --version Print version information and exit.',
- ' -h, --help Show this help message and exit.', ''
- );
- }
- }
- // main
- (new CertificateDownloader())->run();
|