Explorar o código

add: yurunsoft/yurun-oauth-login

joe %!s(int64=4) %!d(string=hai) anos
pai
achega
494659122d
Modificáronse 89 ficheiros con 14129 adicións e 10 borrados
  1. 2 1
      composer.json
  2. 91 1
      composer.lock
  3. 1 1
      vendor/composer/autoload_files.php
  4. 3 1
      vendor/composer/autoload_psr4.php
  5. 16 3
      vendor/composer/autoload_static.php
  6. 96 0
      vendor/composer/installed.json
  7. 20 2
      vendor/composer/installed.php
  8. 1 1
      vendor/services.php
  9. 44 0
      vendor/yurunsoft/yurun-http/.php_cs.dist
  10. 20 0
      vendor/yurunsoft/yurun-http/LICENSE
  11. 323 0
      vendor/yurunsoft/yurun-http/README.md
  12. 32 0
      vendor/yurunsoft/yurun-http/composer.json
  13. 27 0
      vendor/yurunsoft/yurun-http/phpstan.neon
  14. 1171 0
      vendor/yurunsoft/yurun-http/src/HttpRequest.php
  15. 200 0
      vendor/yurunsoft/yurun-http/src/YurunHttp.php
  16. 216 0
      vendor/yurunsoft/yurun-http/src/YurunHttp/Attributes.php
  17. 70 0
      vendor/yurunsoft/yurun-http/src/YurunHttp/Co/Batch.php
  18. 169 0
      vendor/yurunsoft/yurun-http/src/YurunHttp/ConnectionPool.php
  19. 144 0
      vendor/yurunsoft/yurun-http/src/YurunHttp/Cookie/CookieItem.php
  20. 339 0
      vendor/yurunsoft/yurun-http/src/YurunHttp/Cookie/CookieManager.php
  21. 7 0
      vendor/yurunsoft/yurun-http/src/YurunHttp/Exception/WebSocketException.php
  22. 36 0
      vendor/yurunsoft/yurun-http/src/YurunHttp/FormDataBuilder.php
  23. 89 0
      vendor/yurunsoft/yurun-http/src/YurunHttp/Handler/Contract/IConnectionManager.php
  24. 798 0
      vendor/yurunsoft/yurun-http/src/YurunHttp/Handler/Curl.php
  25. 127 0
      vendor/yurunsoft/yurun-http/src/YurunHttp/Handler/Curl/CurlConnectionPool.php
  26. 143 0
      vendor/yurunsoft/yurun-http/src/YurunHttp/Handler/Curl/CurlHttpConnectionManager.php
  27. 63 0
      vendor/yurunsoft/yurun-http/src/YurunHttp/Handler/IHandler.php
  28. 818 0
      vendor/yurunsoft/yurun-http/src/YurunHttp/Handler/Swoole.php
  29. 143 0
      vendor/yurunsoft/yurun-http/src/YurunHttp/Handler/Swoole/SwooleHttp2ConnectionManager.php
  30. 135 0
      vendor/yurunsoft/yurun-http/src/YurunHttp/Handler/Swoole/SwooleHttp2ConnectionPool.php
  31. 143 0
      vendor/yurunsoft/yurun-http/src/YurunHttp/Handler/Swoole/SwooleHttpConnectionManager.php
  32. 128 0
      vendor/yurunsoft/yurun-http/src/YurunHttp/Handler/Swoole/SwooleHttpConnectionPool.php
  33. 396 0
      vendor/yurunsoft/yurun-http/src/YurunHttp/Http/Psr7/AbstractMessage.php
  34. 211 0
      vendor/yurunsoft/yurun-http/src/YurunHttp/Http/Psr7/Consts/MediaType.php
  35. 42 0
      vendor/yurunsoft/yurun-http/src/YurunHttp/Http/Psr7/Consts/RequestHeader.php
  36. 25 0
      vendor/yurunsoft/yurun-http/src/YurunHttp/Http/Psr7/Consts/RequestMethod.php
  37. 47 0
      vendor/yurunsoft/yurun-http/src/YurunHttp/Http/Psr7/Consts/ResponseHeader.php
  38. 147 0
      vendor/yurunsoft/yurun-http/src/YurunHttp/Http/Psr7/Consts/StatusCode.php
  39. 199 0
      vendor/yurunsoft/yurun-http/src/YurunHttp/Http/Psr7/Request.php
  40. 106 0
      vendor/yurunsoft/yurun-http/src/YurunHttp/Http/Psr7/Response.php
  41. 468 0
      vendor/yurunsoft/yurun-http/src/YurunHttp/Http/Psr7/ServerRequest.php
  42. 249 0
      vendor/yurunsoft/yurun-http/src/YurunHttp/Http/Psr7/UploadedFile.php
  43. 590 0
      vendor/yurunsoft/yurun-http/src/YurunHttp/Http/Psr7/Uri.php
  44. 9 0
      vendor/yurunsoft/yurun-http/src/YurunHttp/Http/Request.php
  45. 426 0
      vendor/yurunsoft/yurun-http/src/YurunHttp/Http/Response.php
  46. 128 0
      vendor/yurunsoft/yurun-http/src/YurunHttp/Http2/IHttp2Client.php
  47. 412 0
      vendor/yurunsoft/yurun-http/src/YurunHttp/Http2/SwooleClient.php
  48. 33 0
      vendor/yurunsoft/yurun-http/src/YurunHttp/Pool/BaseConnectionPool.php
  49. 93 0
      vendor/yurunsoft/yurun-http/src/YurunHttp/Pool/Config/PoolConfig.php
  50. 64 0
      vendor/yurunsoft/yurun-http/src/YurunHttp/Pool/Contract/IConnectionPool.php
  51. 59 0
      vendor/yurunsoft/yurun-http/src/YurunHttp/Pool/Traits/TConnectionPoolConfigs.php
  52. 94 0
      vendor/yurunsoft/yurun-http/src/YurunHttp/Random.php
  53. 362 0
      vendor/yurunsoft/yurun-http/src/YurunHttp/Stream/FileStream.php
  54. 281 0
      vendor/yurunsoft/yurun-http/src/YurunHttp/Stream/MemoryStream.php
  55. 53 0
      vendor/yurunsoft/yurun-http/src/YurunHttp/Stream/StreamMode.php
  56. 33 0
      vendor/yurunsoft/yurun-http/src/YurunHttp/Traits/TCookieManager.php
  57. 67 0
      vendor/yurunsoft/yurun-http/src/YurunHttp/Traits/THandler.php
  58. 98 0
      vendor/yurunsoft/yurun-http/src/YurunHttp/WebSocket/IWebSocketClient.php
  59. 193 0
      vendor/yurunsoft/yurun-http/src/YurunHttp/WebSocket/Swoole.php
  60. 20 0
      vendor/yurunsoft/yurun-oauth-login/LICENSE
  61. 132 0
      vendor/yurunsoft/yurun-oauth-login/README.md
  62. 18 0
      vendor/yurunsoft/yurun-oauth-login/composer.json
  63. 285 0
      vendor/yurunsoft/yurun-oauth-login/src/Alipay/OAuth2.php
  64. 85 0
      vendor/yurunsoft/yurun-oauth-login/src/Alipay/loginAgent.html
  65. 7 0
      vendor/yurunsoft/yurun-oauth-login/src/ApiException.php
  66. 196 0
      vendor/yurunsoft/yurun-oauth-login/src/Baidu/OAuth2.php
  67. 96 0
      vendor/yurunsoft/yurun-oauth-login/src/Baidu/loginAgent.html
  68. 280 0
      vendor/yurunsoft/yurun-oauth-login/src/Base.php
  69. 169 0
      vendor/yurunsoft/yurun-oauth-login/src/CSDN/OAuth2.php
  70. 84 0
      vendor/yurunsoft/yurun-oauth-login/src/CSDN/loginAgent.html
  71. 164 0
      vendor/yurunsoft/yurun-oauth-login/src/Coding/OAuth2.php
  72. 84 0
      vendor/yurunsoft/yurun-oauth-login/src/Coding/loginAgent.html
  73. 172 0
      vendor/yurunsoft/yurun-oauth-login/src/Gitee/OAuth2.php
  74. 87 0
      vendor/yurunsoft/yurun-oauth-login/src/Gitee/loginAgent.html
  75. 166 0
      vendor/yurunsoft/yurun-oauth-login/src/Github/OAuth2.php
  76. 87 0
      vendor/yurunsoft/yurun-oauth-login/src/Github/loginAgent.html
  77. 11 0
      vendor/yurunsoft/yurun-oauth-login/src/Lib/Base.php
  78. 88 0
      vendor/yurunsoft/yurun-oauth-login/src/Lib/RSA.php
  79. 67 0
      vendor/yurunsoft/yurun-oauth-login/src/Lib/RSA2.php
  80. 159 0
      vendor/yurunsoft/yurun-oauth-login/src/OSChina/OAuth2.php
  81. 86 0
      vendor/yurunsoft/yurun-oauth-login/src/OSChina/loginAgent.html
  82. 290 0
      vendor/yurunsoft/yurun-oauth-login/src/QQ/OAuth2.php
  83. 16 0
      vendor/yurunsoft/yurun-oauth-login/src/QQ/OpenidMode.php
  84. 87 0
      vendor/yurunsoft/yurun-oauth-login/src/QQ/loginAgent.html
  85. 199 0
      vendor/yurunsoft/yurun-oauth-login/src/Weibo/OAuth2.php
  86. 100 0
      vendor/yurunsoft/yurun-oauth-login/src/Weibo/loginAgent.html
  87. 285 0
      vendor/yurunsoft/yurun-oauth-login/src/Weixin/OAuth2.php
  88. 21 0
      vendor/yurunsoft/yurun-oauth-login/src/Weixin/OpenidMode.php
  89. 88 0
      vendor/yurunsoft/yurun-oauth-login/src/Weixin/loginAgent.html

+ 2 - 1
composer.json

@@ -42,7 +42,8 @@
         "alibabacloud/dysmsapi": "^1.8",
         "alipaysdk/easysdk": "^2.2",
         "pda/pheanstalk": "^4.0",
-        "robthree/twofactorauth": "^1.8"
+        "robthree/twofactorauth": "^1.8",
+        "yurunsoft/yurun-oauth-login": "^3.0"
     },
     "autoload": {
         "psr-4": {

+ 91 - 1
composer.lock

@@ -4,7 +4,7 @@
         "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
         "This file is @generated automatically"
     ],
-    "content-hash": "40251b1b52c7ce9832405c6bc892a1b3",
+    "content-hash": "509db374adaca735f9e7f05c961a661c",
     "packages": [
         {
             "name": "adbario/php-dot-notation",
@@ -3840,6 +3840,96 @@
             ],
             "description": "PHP项目日常开发必备基础库,数组工具类、字符串工具类、数字工具类、函数工具类、服务器工具类、加密工具类",
             "time": "2019-06-22T08:28:23+00:00"
+        },
+        {
+            "name": "yurunsoft/yurun-http",
+            "version": "v4.3.8",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/Yurunsoft/YurunHttp.git",
+                "reference": "b0c80c992c38dbc67ab0f4c59fcac70ad1802785"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/Yurunsoft/YurunHttp/zipball/b0c80c992c38dbc67ab0f4c59fcac70ad1802785",
+                "reference": "b0c80c992c38dbc67ab0f4c59fcac70ad1802785",
+                "shasum": "",
+                "mirrors": [
+                    {
+                        "url": "https://mirrors.aliyun.com/composer/dists/%package%/%reference%.%type%",
+                        "preferred": true
+                    }
+                ]
+            },
+            "require": {
+                "php": ">=5.5.0",
+                "psr/http-message": "~1.0"
+            },
+            "require-dev": {
+                "phpunit/phpunit": ">=4",
+                "swoole/ide-helper": "^4.5",
+                "workerman/workerman": "^4.0"
+            },
+            "type": "library",
+            "autoload": {
+                "psr-4": {
+                    "Yurun\\Util\\": "./src/"
+                }
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "description": "YurunHttp 是开源的 PHP HTTP 类库,支持链式操作,简单易用。支持 Curl、Swoole,支持 Http、Http2、WebSocket!",
+            "support": {
+                "issues": "https://github.com/Yurunsoft/YurunHttp/issues",
+                "source": "https://github.com/Yurunsoft/YurunHttp/tree/v4.3.8"
+            },
+            "time": "2021-07-14T08:01:44+00:00"
+        },
+        {
+            "name": "yurunsoft/yurun-oauth-login",
+            "version": "v3.0.3",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/Yurunsoft/YurunOAuthLogin.git",
+                "reference": "02f764fe917558024ac47f7ae3d93f8dd8a89f9b"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/Yurunsoft/YurunOAuthLogin/zipball/02f764fe917558024ac47f7ae3d93f8dd8a89f9b",
+                "reference": "02f764fe917558024ac47f7ae3d93f8dd8a89f9b",
+                "shasum": "",
+                "mirrors": [
+                    {
+                        "url": "https://mirrors.aliyun.com/composer/dists/%package%/%reference%.%type%",
+                        "preferred": true
+                    }
+                ]
+            },
+            "require": {
+                "php": ">=5.5",
+                "yurunsoft/yurun-http": "~4.0"
+            },
+            "require-dev": {
+                "friendsofphp/php-cs-fixer": "2.18.3"
+            },
+            "type": "library",
+            "autoload": {
+                "psr-4": {
+                    "Yurun\\OAuthLogin\\": "src/"
+                }
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "description": "YurunOAuthLogin是一个PHP 第三方登录授权 SDK,集成了QQ、微信、微博、Github等常用接口。",
+            "support": {
+                "issues": "https://github.com/Yurunsoft/YurunOAuthLogin/issues",
+                "source": "https://github.com/Yurunsoft/YurunOAuthLogin/tree/v3.0.3"
+            },
+            "time": "2021-08-03T08:28:13+00:00"
         }
     ],
     "packages-dev": [

+ 1 - 1
vendor/composer/autoload_files.php

@@ -21,9 +21,9 @@ return array(
     'b067bc7112e384b61c701452d53a14a8' => $vendorDir . '/mtdowling/jmespath.php/src/JmesPath.php',
     '320cde22f66dd4f5d3fd621d3e88b98f' => $vendorDir . '/symfony/polyfill-ctype/bootstrap.php',
     '66453932bc1be9fb2f910a27947d11b6' => $vendorDir . '/alibabacloud/client/src/Functions.php',
+    '6124b4c8570aa390c21fafd04a26c69f' => $vendorDir . '/myclabs/deep-copy/src/DeepCopy/deep_copy.php',
     '0d0b82117c23db94c492fee02b2ed01f' => $vendorDir . '/songshenzong/support/src/StringsHelpers.php',
     'd96a90b43bcdea846705672ffd4e9294' => $vendorDir . '/songshenzong/support/src/BashEchoHelpers.php',
-    '6124b4c8570aa390c21fafd04a26c69f' => $vendorDir . '/myclabs/deep-copy/src/DeepCopy/deep_copy.php',
     '9e090711773bfc38738f5dbaee5a7f14' => $vendorDir . '/overtrue/wechat/src/Payment/helpers.php',
     '841780ea2e1d6545ea3a253239d59c05' => $vendorDir . '/qiniu/php-sdk/src/Qiniu/functions.php',
     '1cfd2761b63b0a29ed23657ea394cb2d' => $vendorDir . '/topthink/think-captcha/src/helper.php',

+ 3 - 1
vendor/composer/autoload_psr4.php

@@ -13,11 +13,13 @@ return array(
     'think\\captcha\\' => array($vendorDir . '/topthink/think-captcha/src'),
     'think\\app\\' => array($vendorDir . '/topthink/think-multi-app/src'),
     'think\\' => array($vendorDir . '/topthink/framework/src/think', $vendorDir . '/topthink/think-factory/src', $vendorDir . '/topthink/think-helper/src', $vendorDir . '/topthink/think-image/src', $vendorDir . '/topthink/think-orm/src', $vendorDir . '/topthink/think-queue/src', $vendorDir . '/topthink/think-template/src'),
-    'phpDocumentor\\Reflection\\' => array($vendorDir . '/phpdocumentor/reflection-common/src', $vendorDir . '/phpdocumentor/type-resolver/src', $vendorDir . '/phpdocumentor/reflection-docblock/src'),
+    'phpDocumentor\\Reflection\\' => array($vendorDir . '/phpdocumentor/reflection-common/src', $vendorDir . '/phpdocumentor/reflection-docblock/src', $vendorDir . '/phpdocumentor/type-resolver/src'),
     'dh2y\\qrcode\\' => array($vendorDir . '/dh2y/think-qrcode/src'),
     'crmeb\\' => array($baseDir . '/crmeb'),
     'clagiordano\\weblibs\\configmanager\\' => array($vendorDir . '/clagiordano/weblibs-configmanager/src'),
     'app\\' => array($baseDir . '/app'),
+    'Yurun\\Util\\' => array($vendorDir . '/yurunsoft/yurun-http/src'),
+    'Yurun\\OAuthLogin\\' => array($vendorDir . '/yurunsoft/yurun-oauth-login/src'),
     'Workerman\\' => array($vendorDir . '/workerman/workerman'),
     'Webmozart\\Assert\\' => array($vendorDir . '/webmozart/assert/src'),
     'Symfony\\Polyfill\\Php72\\' => array($vendorDir . '/symfony/polyfill-php72'),

+ 16 - 3
vendor/composer/autoload_static.php

@@ -22,9 +22,9 @@ class ComposerStaticInitf16474ac994ccc25392f403933800b79
         'b067bc7112e384b61c701452d53a14a8' => __DIR__ . '/..' . '/mtdowling/jmespath.php/src/JmesPath.php',
         '320cde22f66dd4f5d3fd621d3e88b98f' => __DIR__ . '/..' . '/symfony/polyfill-ctype/bootstrap.php',
         '66453932bc1be9fb2f910a27947d11b6' => __DIR__ . '/..' . '/alibabacloud/client/src/Functions.php',
+        '6124b4c8570aa390c21fafd04a26c69f' => __DIR__ . '/..' . '/myclabs/deep-copy/src/DeepCopy/deep_copy.php',
         '0d0b82117c23db94c492fee02b2ed01f' => __DIR__ . '/..' . '/songshenzong/support/src/StringsHelpers.php',
         'd96a90b43bcdea846705672ffd4e9294' => __DIR__ . '/..' . '/songshenzong/support/src/BashEchoHelpers.php',
-        '6124b4c8570aa390c21fafd04a26c69f' => __DIR__ . '/..' . '/myclabs/deep-copy/src/DeepCopy/deep_copy.php',
         '9e090711773bfc38738f5dbaee5a7f14' => __DIR__ . '/..' . '/overtrue/wechat/src/Payment/helpers.php',
         '841780ea2e1d6545ea3a253239d59c05' => __DIR__ . '/..' . '/qiniu/php-sdk/src/Qiniu/functions.php',
         '1cfd2761b63b0a29ed23657ea394cb2d' => __DIR__ . '/..' . '/topthink/think-captcha/src/helper.php',
@@ -62,6 +62,11 @@ class ComposerStaticInitf16474ac994ccc25392f403933800b79
         array (
             'app\\' => 4,
         ),
+        'Y' => 
+        array (
+            'Yurun\\Util\\' => 11,
+            'Yurun\\OAuthLogin\\' => 17,
+        ),
         'W' => 
         array (
             'Workerman\\' => 10,
@@ -195,8 +200,8 @@ class ComposerStaticInitf16474ac994ccc25392f403933800b79
         'phpDocumentor\\Reflection\\' => 
         array (
             0 => __DIR__ . '/..' . '/phpdocumentor/reflection-common/src',
-            1 => __DIR__ . '/..' . '/phpdocumentor/type-resolver/src',
-            2 => __DIR__ . '/..' . '/phpdocumentor/reflection-docblock/src',
+            1 => __DIR__ . '/..' . '/phpdocumentor/reflection-docblock/src',
+            2 => __DIR__ . '/..' . '/phpdocumentor/type-resolver/src',
         ),
         'dh2y\\qrcode\\' => 
         array (
@@ -214,6 +219,14 @@ class ComposerStaticInitf16474ac994ccc25392f403933800b79
         array (
             0 => __DIR__ . '/../..' . '/app',
         ),
+        'Yurun\\Util\\' => 
+        array (
+            0 => __DIR__ . '/..' . '/yurunsoft/yurun-http/src',
+        ),
+        'Yurun\\OAuthLogin\\' => 
+        array (
+            0 => __DIR__ . '/..' . '/yurunsoft/yurun-oauth-login/src',
+        ),
         'Workerman\\' => 
         array (
             0 => __DIR__ . '/..' . '/workerman/workerman',

+ 96 - 0
vendor/composer/installed.json

@@ -6061,6 +6061,102 @@
             ],
             "description": "PHP项目日常开发必备基础库,数组工具类、字符串工具类、数字工具类、函数工具类、服务器工具类、加密工具类",
             "install-path": "../xin/helper"
+        },
+        {
+            "name": "yurunsoft/yurun-http",
+            "version": "v4.3.8",
+            "version_normalized": "4.3.8.0",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/Yurunsoft/YurunHttp.git",
+                "reference": "b0c80c992c38dbc67ab0f4c59fcac70ad1802785"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/Yurunsoft/YurunHttp/zipball/b0c80c992c38dbc67ab0f4c59fcac70ad1802785",
+                "reference": "b0c80c992c38dbc67ab0f4c59fcac70ad1802785",
+                "shasum": "",
+                "mirrors": [
+                    {
+                        "url": "https://mirrors.aliyun.com/composer/dists/%package%/%reference%.%type%",
+                        "preferred": true
+                    }
+                ]
+            },
+            "require": {
+                "php": ">=5.5.0",
+                "psr/http-message": "~1.0"
+            },
+            "require-dev": {
+                "phpunit/phpunit": ">=4",
+                "swoole/ide-helper": "^4.5",
+                "workerman/workerman": "^4.0"
+            },
+            "time": "2021-07-14T08:01:44+00:00",
+            "type": "library",
+            "installation-source": "dist",
+            "autoload": {
+                "psr-4": {
+                    "Yurun\\Util\\": "./src/"
+                }
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "description": "YurunHttp 是开源的 PHP HTTP 类库,支持链式操作,简单易用。支持 Curl、Swoole,支持 Http、Http2、WebSocket!",
+            "support": {
+                "issues": "https://github.com/Yurunsoft/YurunHttp/issues",
+                "source": "https://github.com/Yurunsoft/YurunHttp/tree/v4.3.8"
+            },
+            "install-path": "../yurunsoft/yurun-http"
+        },
+        {
+            "name": "yurunsoft/yurun-oauth-login",
+            "version": "v3.0.3",
+            "version_normalized": "3.0.3.0",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/Yurunsoft/YurunOAuthLogin.git",
+                "reference": "02f764fe917558024ac47f7ae3d93f8dd8a89f9b"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/Yurunsoft/YurunOAuthLogin/zipball/02f764fe917558024ac47f7ae3d93f8dd8a89f9b",
+                "reference": "02f764fe917558024ac47f7ae3d93f8dd8a89f9b",
+                "shasum": "",
+                "mirrors": [
+                    {
+                        "url": "https://mirrors.aliyun.com/composer/dists/%package%/%reference%.%type%",
+                        "preferred": true
+                    }
+                ]
+            },
+            "require": {
+                "php": ">=5.5",
+                "yurunsoft/yurun-http": "~4.0"
+            },
+            "require-dev": {
+                "friendsofphp/php-cs-fixer": "2.18.3"
+            },
+            "time": "2021-08-03T08:28:13+00:00",
+            "type": "library",
+            "installation-source": "dist",
+            "autoload": {
+                "psr-4": {
+                    "Yurun\\OAuthLogin\\": "src/"
+                }
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "description": "YurunOAuthLogin是一个PHP 第三方登录授权 SDK,集成了QQ、微信、微博、Github等常用接口。",
+            "support": {
+                "issues": "https://github.com/Yurunsoft/YurunOAuthLogin/issues",
+                "source": "https://github.com/Yurunsoft/YurunOAuthLogin/tree/v3.0.3"
+            },
+            "install-path": "../yurunsoft/yurun-oauth-login"
         }
     ],
     "dev": true,

+ 20 - 2
vendor/composer/installed.php

@@ -5,7 +5,7 @@
         'type' => 'project',
         'install_path' => __DIR__ . '/../../',
         'aliases' => array(),
-        'reference' => '69576648b380b8a6b9b1a480bfea6ac976a54260',
+        'reference' => 'a1583401a20253e1e3349cb88e248cf227fc37f4',
         'name' => 'topthink/think',
         'dev' => true,
     ),
@@ -871,7 +871,7 @@
             'type' => 'project',
             'install_path' => __DIR__ . '/../../',
             'aliases' => array(),
-            'reference' => '69576648b380b8a6b9b1a480bfea6ac976a54260',
+            'reference' => 'a1583401a20253e1e3349cb88e248cf227fc37f4',
             'dev_requirement' => false,
         ),
         'topthink/think-captcha' => array(
@@ -1009,5 +1009,23 @@
             'reference' => '02a58132dae2aea2d1c0b8e66f55125969224747',
             'dev_requirement' => false,
         ),
+        'yurunsoft/yurun-http' => array(
+            'pretty_version' => 'v4.3.8',
+            'version' => '4.3.8.0',
+            'type' => 'library',
+            'install_path' => __DIR__ . '/../yurunsoft/yurun-http',
+            'aliases' => array(),
+            'reference' => 'b0c80c992c38dbc67ab0f4c59fcac70ad1802785',
+            'dev_requirement' => false,
+        ),
+        'yurunsoft/yurun-oauth-login' => array(
+            'pretty_version' => 'v3.0.3',
+            'version' => '3.0.3.0',
+            'type' => 'library',
+            'install_path' => __DIR__ . '/../yurunsoft/yurun-oauth-login',
+            'aliases' => array(),
+            'reference' => '02f764fe917558024ac47f7ae3d93f8dd8a89f9b',
+            'dev_requirement' => false,
+        ),
     ),
 );

+ 1 - 1
vendor/services.php

@@ -1,5 +1,5 @@
 <?php 
-// This file is automatically generated at:2021-10-01 18:54:11
+// This file is automatically generated at:2021-10-04 13:46:48
 declare (strict_types = 1);
 return array (
   0 => 'think\\captcha\\CaptchaService',

+ 44 - 0
vendor/yurunsoft/yurun-http/.php_cs.dist

@@ -0,0 +1,44 @@
+<?php
+
+if (!file_exists(__DIR__ . '/src'))
+{
+    exit(0);
+}
+
+return PhpCsFixer\Config::create()
+    ->setRules([
+        '@Symfony'                   => true,
+        '@Symfony:risky'             => true,
+        'php_unit_dedicate_assert'   => ['target' => '5.6'],
+        'array_syntax'               => ['syntax' => 'short'],
+        'array_indentation'          => true,
+        'binary_operator_spaces'     => [
+            'operators' => [
+                '=>' => 'align_single_space',
+            ],
+        ],
+        'concat_space' => [
+            'spacing' => 'one',
+        ],
+        'fopen_flags'                => false,
+        'protected_to_private'       => false,
+        'native_constant_invocation' => true,
+        'single_quote'               => true,
+        'braces'                     => [
+            'position_after_control_structures' => 'next',
+        ],
+        'no_superfluous_phpdoc_tags'   => false,
+        'single_line_comment_style'    => false,
+        'combine_nested_dirname'       => false,
+        'backtick_to_shell_exec'       => false,
+    ])
+    ->setRiskyAllowed(true)
+    ->setFinder(
+        PhpCsFixer\Finder::create()
+            ->exclude(__DIR__ . '/vendor')
+            ->in(__DIR__ . '/src')
+            ->in(__DIR__ . '/examples')
+            ->in(__DIR__ . '/tests')
+            ->append([__FILE__])
+    )
+;

+ 20 - 0
vendor/yurunsoft/yurun-http/LICENSE

@@ -0,0 +1,20 @@
+The MIT License (MIT)
+
+Copyright (c) 2017 宇润
+
+Permission is hereby granted, free of charge, to any person obtaining a copy of
+this software and associated documentation files (the "Software"), to deal in
+the Software without restriction, including without limitation the rights to
+use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
+the Software, and to permit persons to whom the Software is furnished to do so,
+subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
+FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
+COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
+IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
+CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

+ 323 - 0
vendor/yurunsoft/yurun-http/README.md

@@ -0,0 +1,323 @@
+# YurunHttp
+
+[![Latest Version](https://poser.pugx.org/yurunsoft/yurun-http/v/stable)](https://packagist.org/packages/yurunsoft/yurun-http)
+![GitHub Workflow Status (branch)](https://img.shields.io/github/workflow/status/Yurunsoft/YurunHttp/ci/dev)
+[![Php Version](https://img.shields.io/badge/php-%3E=5.5-brightgreen.svg)](https://secure.php.net/)
+[![IMI Doc](https://img.shields.io/badge/docs-passing-green.svg)](http://doc.yurunsoft.com/YurunHttp)
+[![IMI License](https://img.shields.io/github/license/Yurunsoft/YurunHttp.svg)](https://github.com/Yurunsoft/YurunHttp/blob/master/LICENSE)
+
+## 简介
+
+YurunHttp,支持智能识别 Curl/Swoole 场景的高性能 Http Client。
+
+支持链式操作,简单易用。支持并发批量请求、HTTP2、WebSocket 全双工通信协议。
+
+非常适合用于开发通用 SDK 包,不必再为 Swoole 协程兼容而头疼!
+
+YurunHttp 的目标是做最好用的 PHP HTTP Client 开发包!
+
+### 特性
+
+* GET/POST/PUT/DELETE/UPDATE 等请求方式
+* 浏览器级别 Cookies 管理
+* 上传及下载
+* 请求头和响应头
+* 失败重试
+* 自动重定向
+* HTTP 代理方式请求
+* SSL 证书(HTTPS)
+* 并发批量请求
+* HTTP2
+* WebSocket
+* Curl & Swoole 环境智能兼容
+* 连接池
+
+---
+
+开发手册文档:<https://doc.yurunsoft.com/YurunHttp>
+
+API 文档:[https://apidoc.gitee.com/yurunsoft/YurunHttp](https://apidoc.gitee.com/yurunsoft/YurunHttp)
+
+欢迎各位加入技术支持群17916227[![点击加群](https://pub.idqqimg.com/wpa/images/group.png "点击加群")](https://jq.qq.com/?_wv=1027&k=5wXf4Zq),如有问题可以及时解答和修复。
+
+更加欢迎各位来提交PR([码云](https://gitee.com/yurunsoft/YurunHttp)/[Github](https://github.com/Yurunsoft/YurunHttp)),一起完善YurunHttp,让它能够更加好用。
+
+## 重大版本更新日志
+
+> 每个小版本的更新日志请移步到 Release 查看
+
+v4.3.0 新增支持连接池
+
+v4.2.0 重构 Swoole 处理器,并发请求性能大幅提升 (PHP 版本依赖降为 >= 5.5)
+
+v4.1.0 实现智能识别场景,自动选择适合 Curl/Swoole 环境的处理器
+
+v4.0.0 新增支持 `Swoole` 并发批量请求 (PHP >= 7.1)
+
+v3.5.0 新增支持 `Curl` 并发批量请求 (PHP >= 5.5)
+
+v3.4.0 新增支持 `Http2` 全双工用法
+
+v3.3.0 新增支持 `Http2` 兼容用法
+
+v3.2.0 新增支持 `Swoole WebSocket` 客户端
+
+v3.1.0 引入浏览器级别 `Cookies` 管理
+
+v3.0.0 新增支持 `Swoole` 协程
+
+v2.0.0 黑历史,不告诉你
+
+v1.3.1 支持 `Composer`
+
+v1.0-1.3 初期版本迭代
+
+## Composer
+
+本项目可以使用composer安装,遵循psr-4自动加载规则,在你的 `composer.json` 中加入下面的内容
+
+```json
+{
+    "require": {
+        "yurunsoft/yurun-http": "^4.3.0"
+    }
+}
+```
+
+然后执行 `composer update` 安装。
+
+之后你便可以使用 `include "vendor/autoload.php";` 来自动加载类。(ps:不要忘了namespace)
+
+## 用法
+
+更加详细的用法请看 `examples` 目录中的示例代码
+
+### 简单调用
+
+```php
+<?php
+use Yurun\Util\HttpRequest;
+
+$http = new HttpRequest;
+
+// 设置 Header 4 种方法
+$http->header('aaa', 'value1')
+     ->headers([
+         'bbb' => 'value2',
+         'ccc' => 'value3',
+     ])
+     ->rawHeader('ddd:value4')
+     ->rawHeaders([
+         'eee:value5',
+         'fff:value6',
+     ]);
+
+// 请求
+$response = $http->ua('YurunHttp')
+                 ->get('http://www.baidu.com');
+
+echo 'html:', PHP_EOL, $response->body();
+```
+
+### 并发批量请求
+
+```php
+use \Yurun\Util\YurunHttp\Co\Batch;
+use \Yurun\Util\HttpRequest;
+
+$result = Batch::run([
+    (new HttpRequest)->url('https://www.imiphp.com'),
+    (new HttpRequest)->url('https://www.yurunsoft.com'),
+]);
+
+var_dump($result[0]->getHeaders(), strlen($result[0]->body()), $result[0]->getStatusCode());
+
+var_dump($result[1]->getHeaders(), strlen($result[1]->body()), $result[1]->getStatusCode());
+```
+
+> 只有 Swoole 并发请求会受到连接池限制,Curl 不受影响
+
+### Swoole 协程模式
+
+```php
+<?php
+use Yurun\Util\YurunHttp;
+use Yurun\Util\HttpRequest;
+
+// Swoole 处理器必须在协程中调用
+go('test');
+
+function test()
+{
+    $http = new HttpRequest;
+    $response = $http->get('http://www.baidu.com');
+    echo 'html:', PHP_EOL, $response->body();
+}
+```
+
+### 连接池
+
+在 YurunHttp 中,连接池是全局的,默认不启用。
+
+每个不同的 `host`、`port`、`ssl` 都在不同的连接池中,举个例子,下面两个 url 对应的连接池不是同一个:
+
+`http://www.imiphp.com`(`host=www.imiphp.com, port=80, ssl=false`)
+
+`https://www.imiphp.com`(`host=www.imiphp.com, port=443, ssl=true`)
+
+**启用全局连接池:**
+
+```php
+\Yurun\Util\YurunHttp\ConnectionPool::enable();
+```
+
+**禁用全局连接池:**
+
+```php
+\Yurun\Util\YurunHttp\ConnectionPool::disable();
+```
+
+**写入连接池设置:**
+
+```php
+// 最大连接数=16个,连接数满等待超时时间(仅 Swoole 有效)=30s
+// url 最后不要带斜杠 /
+\Yurun\Util\YurunHttp\ConnectionPool::setConfig('https://imiphp.com', 16, 30);
+```
+
+> YurunHttp 不会限制未设置的域名的连接数
+
+**特殊请求不启用连接池:**
+
+```php
+$http = new HttpRequest;
+$http->connectionPool(false);
+```
+
+**获取连接池对象及数据:**
+
+```php
+use Yurun\Util\YurunHttp\Handler\Curl\CurlHttpConnectionManager;
+use Yurun\Util\YurunHttp\Handler\Swoole\SwooleHttpConnectionManager;
+
+// 获取 Curl 连接池管理器,选择你所处环境对应的类,其实一般 Curl 不太需要连接池
+// $manager = CurlHttpConnectionManager::getInstance();
+
+// 获取 Swoole 连接池管理器,选择你所处环境对应的类
+$manager = SwooleHttpConnectionManager::getInstance();
+
+// 获取连接池对象集合
+$pool = $manager->getConnectionPool('https://imiphp.com');
+
+// 获取连接总数
+$pool->getCount();
+
+// 获取空闲连接总数
+$pool->getFree();
+
+// 获取正在使用的连接总数
+$pool->getUsed();
+
+// 获取连接池配置
+$config = $pool->getConfig();
+```
+
+### WebSocket Client
+
+```php
+go(function(){
+    $url = 'ws://127.0.0.1:1234/';
+    $http = new HttpRequest;
+    $client = $http->websocket($url);
+    if(!$client->isConnected())
+    {
+        throw new \RuntimeException('Connect failed');
+    }
+    $client->send('data');
+    $recv = $client->recv();
+    var_dump('recv:', $recv);
+    $client->close();
+});
+```
+
+### Http2 兼容用法
+
+```php
+$http = new HttpRequest;
+$http->protocolVersion = '2.0'; // 这句是关键
+$response = $http->get('https://wiki.swoole.com/');
+```
+
+Curl、Swoole Handler 都支持 Http2,但需要注意的是编译时都需要带上启用 Http2 的参数。
+
+查看是否支持:
+
+Curl: `php --ri curl`
+
+Swoole: `php --ri swoole`
+
+### Http2 全双工用法
+
+> 该用法仅支持 Swoole
+
+```php
+$uri = new Uri('https://wiki.swoole.com/');
+
+// 客户端初始化和连接
+$client = new \Yurun\Util\YurunHttp\Http2\SwooleClient($uri->getHost(), Uri::getServerPort($uri), 'https' === $uri->getScheme());
+$client->connect();
+
+// 请求构建
+$httpRequest = new HttpRequest;
+$request = $httpRequest->header('aaa', 'bbb')->buildRequest($uri, [
+    'date'  =>  $i,
+], 'POST', 'json');
+
+for($i = 0; $i < 10; ++$i)
+{
+    go(function() use($client, $request){
+        // 发送(支持在多个协程执行)
+        $streamId = $client->send($request);
+        var_dump('send:' . $streamId);
+
+        // 接收(支持在多个协程执行)
+        $response = $client->recv($streamId, 3);
+        $content = $response->body();
+        var_dump($response);
+    });
+}
+```
+
+> 具体用法请看 `examples/http2Client.php`
+
+### PSR-7 请求构建
+
+```php
+<?php
+use Yurun\Util\YurunHttp\Http\Request;
+use Yurun\Util\YurunHttp;
+
+$url = 'http://www.baidu.com';
+
+// 构造方法定义:__construct($uri = null, array $headers = [], $body = '', $method = RequestMethod::GET, $version = '1.1', array $server = [], array $cookies = [], array $files = [])
+$request = new Request($url);
+
+// 发送请求并获取结果
+$response = YurunHttp::send($request);
+
+var_dump($response);
+```
+
+## 商业合作
+
+现在使用 Swoole 的项目越来越多,使用 YurunHttp 开发的代码,可以原生兼容 php-fpm 和 Swoole 两种环境。
+
+YurunHttp 相比 Guzzle 性能更强,功能更加强大!
+
+现承接使用 PHP 开发相关系统、SDK 等业务,有需要的请联系 QQ: 369124067
+
+## 捐赠
+
+<img src="https://raw.githubusercontent.com/Yurunsoft/YurunHttp/master/res/pay.png"/>
+
+开源不求盈利,多少都是心意,生活不易,随缘随缘……

+ 32 - 0
vendor/yurunsoft/yurun-http/composer.json

@@ -0,0 +1,32 @@
+{
+    "name": "yurunsoft/yurun-http",
+    "description": "YurunHttp 是开源的 PHP HTTP 类库,支持链式操作,简单易用。支持 Curl、Swoole,支持 Http、Http2、WebSocket!",
+    "require": {
+        "php": ">=5.5.0",
+        "psr/http-message": "~1.0"
+    },
+    "require-dev": {
+        "swoole/ide-helper": "^4.5",
+        "phpunit/phpunit": ">=4",
+        "workerman/workerman": "^4.0"
+    },
+    "type": "library",
+    "license": "MIT",
+    "autoload": {
+        "psr-4": {
+            "Yurun\\Util\\": "./src/"
+        }
+    },
+    "autoload-dev": {
+        "psr-4": {
+            "Yurun\\Util\\YurunHttp\\Test\\": "tests/unit/"
+        }
+    },
+    "scripts": {
+        "test": "./vendor/bin/phpunit -c ./tests/phpunit.xml",
+        "install-test": [
+            "@composer install",
+            "@composer test"
+        ]
+    }
+}

+ 27 - 0
vendor/yurunsoft/yurun-http/phpstan.neon

@@ -0,0 +1,27 @@
+parameters:
+    level: 6
+
+    paths:
+        - src
+        - tests
+
+    bootstrapFiles:
+        - vendor/autoload.php
+
+    excludePaths:
+        - vendor
+    
+    treatPhpDocTypesAsCertain: false
+    checkGenericClassInNonGenericObjectType: false
+
+    ignoreErrors:
+        - '#Unsafe usage of new static\(\).+#'
+        - '#Method \S+ return type has no value type specified in iterable type array.#'
+        - '#Method \S+ has parameter \S+ with no value type specified in iterable type array.#'
+        - '#Property \S+ type has no value type specified in iterable type array.#'
+        - '#Parameter \#\d+ \$\S+ of method Workerman\\[^:]+::[^\(]+\(\) expects [^,]+, \S+ given.#'
+        - '#Access to an undefined property Workerman\\Connection\\TcpConnection::\$__request.#'
+        -
+            message: '#Method \S+ has no return typehint specified.#'
+            paths:
+                - tests/unit/**Test.php

+ 1171 - 0
vendor/yurunsoft/yurun-http/src/HttpRequest.php

@@ -0,0 +1,1171 @@
+<?php
+
+namespace Yurun\Util;
+
+use Yurun\Util\YurunHttp\Attributes;
+use Yurun\Util\YurunHttp\Http\Psr7\Consts\MediaType;
+use Yurun\Util\YurunHttp\Http\Psr7\UploadedFile;
+use Yurun\Util\YurunHttp\Http\Request;
+
+class HttpRequest
+{
+    /**
+     * 处理器.
+     *
+     * @var \Yurun\Util\YurunHttp\Handler\IHandler|null
+     */
+    private $handler;
+
+    /**
+     * 需要请求的Url地址
+     *
+     * @var string
+     */
+    public $url;
+
+    /**
+     * 发送内容,可以是字符串、数组(支持键值、Yurun\Util\YurunHttp\Http\Psr7\UploadedFile,其中键值会作为html编码,文件则是上传).
+     *
+     * @var mixed
+     */
+    public $content;
+
+    /**
+     * `curl_setopt_array()`所需要的第二个参数.
+     *
+     * @var array
+     */
+    public $options = [];
+
+    /**
+     * 请求头.
+     *
+     * @var array
+     */
+    public $headers = [];
+
+    /**
+     * Cookies.
+     *
+     * @var array
+     */
+    public $cookies = [];
+
+    /**
+     * 失败重试次数,默认为0.
+     *
+     * @var int
+     */
+    public $retry = 0;
+
+    /**
+     * 是否使用代理,默认false.
+     *
+     * @var bool
+     */
+    public $useProxy = false;
+
+    /**
+     * 代理设置.
+     *
+     * @var array
+     */
+    public $proxy = [];
+
+    /**
+     * 是否验证证书.
+     *
+     * @var bool
+     */
+    public $isVerifyCA = false;
+
+    /**
+     * CA根证书路径.
+     *
+     * @var string|null
+     */
+    public $caCert;
+
+    /**
+     * 连接超时时间,单位:毫秒.
+     *
+     * @var int
+     */
+    public $connectTimeout = 30000;
+
+    /**
+     * 总超时时间,单位:毫秒.
+     *
+     * @var int
+     */
+    public $timeout = 30000;
+
+    /**
+     * 下载限速,为0则不限制,单位:字节
+     *
+     * @var int|null
+     */
+    public $downloadSpeed;
+
+    /**
+     * 上传限速,为0则不限制,单位:字节
+     *
+     * @var int|null
+     */
+    public $uploadSpeed;
+
+    /**
+     * 用于连接中需要的用户名.
+     *
+     * @var string|null
+     */
+    public $username;
+
+    /**
+     * 用于连接中需要的密码
+     *
+     * @var string|null
+     */
+    public $password;
+
+    /**
+     * 请求结果保存至文件的配置.
+     *
+     * @var mixed
+     */
+    public $saveFileOption = [];
+
+    /**
+     * 是否启用重定向.
+     *
+     * @var bool
+     */
+    public $followLocation = true;
+
+    /**
+     * 最大重定向次数.
+     *
+     * @var int
+     */
+    public $maxRedirects = 10;
+
+    /**
+     * 证书类型
+     * 支持的格式有"PEM" (默认值), "DER"和"ENG".
+     *
+     * @var string
+     */
+    public $certType = 'pem';
+
+    /**
+     * 一个包含 PEM 格式证书的文件名.
+     *
+     * @var string
+     */
+    public $certPath = '';
+    /**
+     * 使用证书需要的密码
+     *
+     * @var string
+     */
+    public $certPassword = null;
+
+    /**
+     * certType规定的私钥的加密类型,支持的密钥类型为"PEM"(默认值)、"DER"和"ENG".
+     *
+     * @var string
+     */
+    public $keyType = 'pem';
+
+    /**
+     * 包含 SSL 私钥的文件名.
+     *
+     * @var string
+     */
+    public $keyPath = '';
+
+    /**
+     * SSL私钥的密码
+     *
+     * @var string
+     */
+    public $keyPassword = null;
+
+    /**
+     * 请求方法.
+     *
+     * @var string
+     */
+    public $method = 'GET';
+
+    /**
+     * Http 协议版本.
+     *
+     * @var string
+     */
+    public $protocolVersion = '1.1';
+
+    /**
+     * 是否启用连接池,默认为 null 时取全局设置.
+     *
+     * @var bool|null
+     */
+    public $connectionPool = null;
+
+    /**
+     * 代理认证方式.
+     *
+     * @var array
+     */
+    public static $proxyAuths = [];
+
+    /**
+     * 代理类型.
+     *
+     * @var array
+     */
+    public static $proxyType = [];
+
+    /**
+     * 自动扩展名标志.
+     */
+    const AUTO_EXT_FLAG = '.*';
+
+    /**
+     * 自动扩展名用的临时文件名.
+     */
+    const AUTO_EXT_TEMP_EXT = '.tmp';
+
+    /**
+     * 构造方法.
+     *
+     * @return mixed
+     */
+    public function __construct()
+    {
+        $this->open();
+    }
+
+    /**
+     * 析构方法.
+     */
+    public function __destruct()
+    {
+        $this->close();
+    }
+
+    /**
+     * 打开一个新连接,初始化所有参数。一般不需要手动调用。
+     *
+     * @return void
+     */
+    public function open()
+    {
+        $this->handler = YurunHttp::getHandler();
+        $this->retry = 0;
+        $this->headers = $this->options = [];
+        $this->url = $this->content = '';
+        $this->useProxy = false;
+        $this->proxy = [
+            'auth'    => 'basic',
+            'type'    => 'http',
+        ];
+        $this->isVerifyCA = false;
+        $this->caCert = null;
+        $this->connectTimeout = 30000;
+        $this->timeout = 30000;
+        $this->downloadSpeed = null;
+        $this->uploadSpeed = null;
+        $this->username = null;
+        $this->password = null;
+        $this->saveFileOption = [];
+    }
+
+    /**
+     * 关闭连接。一般不需要手动调用。
+     *
+     * @return void
+     */
+    public function close()
+    {
+        if ($this->handler)
+        {
+            $handler = $this->handler;
+            $this->handler = null;
+            $handler->close();
+        }
+    }
+
+    /**
+     * 创建一个新会话,等同于new.
+     *
+     * @return static
+     */
+    public static function newSession()
+    {
+        return new static();
+    }
+
+    /**
+     * 获取处理器.
+     *
+     * @return \Yurun\Util\YurunHttp\Handler\IHandler|null
+     */
+    public function getHandler()
+    {
+        return $this->handler;
+    }
+
+    /**
+     * 设置请求地址
+     *
+     * @param string $url 请求地址
+     *
+     * @return static
+     */
+    public function url($url)
+    {
+        $this->url = $url;
+
+        return $this;
+    }
+
+    /**
+     * 设置发送内容,requestBody的别名.
+     *
+     * @param mixed $content 发送内容,可以是字符串、数组
+     *
+     * @return static
+     */
+    public function content($content)
+    {
+        return $this->requestBody($content);
+    }
+
+    /**
+     * 设置参数,requestBody的别名.
+     *
+     * @param mixed $params 发送内容,可以是字符串、数组
+     *
+     * @return static
+     */
+    public function params($params)
+    {
+        return $this->requestBody($params);
+    }
+
+    /**
+     * 设置请求主体.
+     *
+     * @param string|string|array $requestBody 发送内容,可以是字符串、数组
+     *
+     * @return static
+     */
+    public function requestBody($requestBody)
+    {
+        $this->content = $requestBody;
+
+        return $this;
+    }
+
+    /**
+     * 批量设置CURL的Option.
+     *
+     * @param array $options curl_setopt_array()所需要的第二个参数
+     *
+     * @return static
+     */
+    public function options($options)
+    {
+        $thisOptions = &$this->options;
+        foreach ($options as $key => $value)
+        {
+            $thisOptions[$key] = $value;
+        }
+
+        return $this;
+    }
+
+    /**
+     * 设置CURL的Option.
+     *
+     * @param int   $option 需要设置的CURLOPT_XXX选项
+     * @param mixed $value  值
+     *
+     * @return static
+     */
+    public function option($option, $value)
+    {
+        $this->options[$option] = $value;
+
+        return $this;
+    }
+
+    /**
+     * 批量设置请求头.
+     *
+     * @param array $headers 键值数组
+     *
+     * @return static
+     */
+    public function headers($headers)
+    {
+        $thisHeaders = &$this->headers;
+        $thisHeaders = array_merge($thisHeaders, $headers);
+
+        return $this;
+    }
+
+    /**
+     * 设置请求头.
+     *
+     * @param string $header 请求头名称
+     * @param string $value  值
+     *
+     * @return static
+     */
+    public function header($header, $value)
+    {
+        $this->headers[$header] = $value;
+
+        return $this;
+    }
+
+    /**
+     * 批量设置请求头,.
+     *
+     * @param array $headers 纯文本 header 数组
+     *
+     * @return static
+     */
+    public function rawHeaders($headers)
+    {
+        $thisHeaders = &$this->headers;
+        foreach ($headers as $header)
+        {
+            $list = explode(':', $header, 2);
+            $thisHeaders[trim($list[0])] = trim($list[1]);
+        }
+
+        return $this;
+    }
+
+    /**
+     * 设置请求头.
+     *
+     * @param string $header 纯文本 header
+     *
+     * @return static
+     */
+    public function rawHeader($header)
+    {
+        $list = explode(':', $header, 2);
+        $this->headers[trim($list[0])] = trim($list[1]);
+
+        return $this;
+    }
+
+    /**
+     * 设置Accept.
+     *
+     * @param string $accept
+     *
+     * @return static
+     */
+    public function accept($accept)
+    {
+        $this->headers['Accept'] = $accept;
+
+        return $this;
+    }
+
+    /**
+     * 设置Accept-Language.
+     *
+     * @param string $acceptLanguage
+     *
+     * @return static
+     */
+    public function acceptLanguage($acceptLanguage)
+    {
+        $this->headers['Accept-Language'] = $acceptLanguage;
+
+        return $this;
+    }
+
+    /**
+     * 设置Accept-Encoding.
+     *
+     * @param string $acceptEncoding
+     *
+     * @return static
+     */
+    public function acceptEncoding($acceptEncoding)
+    {
+        $this->headers['Accept-Encoding'] = $acceptEncoding;
+
+        return $this;
+    }
+
+    /**
+     * 设置Accept-Ranges.
+     *
+     * @param string $acceptRanges
+     *
+     * @return static
+     */
+    public function acceptRanges($acceptRanges)
+    {
+        $this->headers['Accept-Ranges'] = $acceptRanges;
+
+        return $this;
+    }
+
+    /**
+     * 设置Cache-Control.
+     *
+     * @param string $cacheControl
+     *
+     * @return static
+     */
+    public function cacheControl($cacheControl)
+    {
+        $this->headers['Cache-Control'] = $cacheControl;
+
+        return $this;
+    }
+
+    /**
+     * 批量设置Cookies.
+     *
+     * @param array $cookies 键值对应数组
+     *
+     * @return static
+     */
+    public function cookies($cookies)
+    {
+        $this->cookies = array_merge($this->cookies, $cookies);
+
+        return $this;
+    }
+
+    /**
+     * 设置Cookie.
+     *
+     * @param string $name  名称
+     * @param string $value 值
+     *
+     * @return static
+     */
+    public function cookie($name, $value)
+    {
+        $this->cookies[$name] = $value;
+
+        return $this;
+    }
+
+    /**
+     * 设置Content-Type.
+     *
+     * @param string $contentType
+     *
+     * @return static
+     */
+    public function contentType($contentType)
+    {
+        $this->headers['Content-Type'] = $contentType;
+
+        return $this;
+    }
+
+    /**
+     * 设置Range.
+     *
+     * @param string $range
+     *
+     * @return static
+     */
+    public function range($range)
+    {
+        $this->headers['Range'] = $range;
+
+        return $this;
+    }
+
+    /**
+     * 设置Referer.
+     *
+     * @param string $referer
+     *
+     * @return static
+     */
+    public function referer($referer)
+    {
+        $this->headers['Referer'] = $referer;
+
+        return $this;
+    }
+
+    /**
+     * 设置User-Agent.
+     *
+     * @param string $userAgent
+     *
+     * @return static
+     */
+    public function userAgent($userAgent)
+    {
+        $this->headers['User-Agent'] = $userAgent;
+
+        return $this;
+    }
+
+    /**
+     * 设置User-Agent,userAgent的别名.
+     *
+     * @param string $userAgent
+     *
+     * @return static
+     */
+    public function ua($userAgent)
+    {
+        return $this->userAgent($userAgent);
+    }
+
+    /**
+     * 设置失败重试次数,状态码为5XX或者0才需要重试.
+     *
+     * @param string $retry
+     *
+     * @return static
+     */
+    public function retry($retry)
+    {
+        $this->retry = $retry < 0 ? 0 : $retry;   //至少请求1次,即重试0次
+
+        return $this;
+    }
+
+    /**
+     * 代理.
+     *
+     * @param string $server 代理服务器地址
+     * @param int    $port   代理服务器端口
+     * @param string $type   代理类型,支持:http、socks4、socks4a、socks5
+     * @param string $auth   代理认证方式,支持:basic、ntlm。一般默认basic
+     *
+     * @return static
+     */
+    public function proxy($server, $port, $type = 'http', $auth = 'basic')
+    {
+        $this->useProxy = true;
+        $this->proxy = [
+            'server'    => $server,
+            'port'      => $port,
+            'type'      => $type,
+            'auth'      => $auth,
+        ];
+
+        return $this;
+    }
+
+    /**
+     * 代理认证
+     *
+     * @param string $username
+     * @param string $password
+     *
+     * @return static
+     */
+    public function proxyAuth($username, $password)
+    {
+        $this->proxy['username'] = $username;
+        $this->proxy['password'] = $password;
+
+        return $this;
+    }
+
+    /**
+     * 设置超时时间.
+     *
+     * @param int $timeout        总超时时间,单位:毫秒
+     * @param int $connectTimeout 连接超时时间,单位:毫秒
+     *
+     * @return static
+     */
+    public function timeout($timeout = null, $connectTimeout = null)
+    {
+        if (null !== $timeout)
+        {
+            $this->timeout = $timeout;
+        }
+        if (null !== $connectTimeout)
+        {
+            $this->connectTimeout = $connectTimeout;
+        }
+
+        return $this;
+    }
+
+    /**
+     * 限速
+     *
+     * @param int $download 下载速度,为0则不限制,单位:字节
+     * @param int $upload   上传速度,为0则不限制,单位:字节
+     *
+     * @return static
+     */
+    public function limitRate($download = 0, $upload = 0)
+    {
+        $this->downloadSpeed = $download;
+        $this->uploadSpeed = $upload;
+
+        return $this;
+    }
+
+    /**
+     * 设置用于连接中需要的用户名和密码
+     *
+     * @param string $username 用户名
+     * @param string $password 密码
+     *
+     * @return static
+     */
+    public function userPwd($username, $password)
+    {
+        $this->username = $username;
+        $this->password = $password;
+
+        return $this;
+    }
+
+    /**
+     * 保存至文件的设置.
+     *
+     * @param string $filePath 文件路径
+     * @param string $fileMode 文件打开方式,默认w+
+     *
+     * @return static
+     */
+    public function saveFile($filePath, $fileMode = 'w+')
+    {
+        $this->saveFileOption['filePath'] = $filePath;
+        $this->saveFileOption['fileMode'] = $fileMode;
+
+        return $this;
+    }
+
+    /**
+     * 获取文件保存路径.
+     *
+     * @return string|null
+     */
+    public function getSavePath()
+    {
+        $saveFileOption = $this->saveFileOption;
+
+        return isset($saveFileOption['filePath']) ? $saveFileOption['filePath'] : null;
+    }
+
+    /**
+     * 设置SSL证书.
+     *
+     * @param string $path     一个包含 PEM 格式证书的文件名
+     * @param string $type     证书类型,支持的格式有”PEM”(默认值),“DER”和”ENG”
+     * @param string $password 使用证书需要的密码
+     *
+     * @return static
+     */
+    public function sslCert($path, $type = null, $password = null)
+    {
+        $this->certPath = $path;
+        if (null !== $type)
+        {
+            $this->certType = $type;
+        }
+        if (null !== $password)
+        {
+            $this->certPassword = $password;
+        }
+
+        return $this;
+    }
+
+    /**
+     * 设置SSL私钥.
+     *
+     * @param string $path     包含 SSL 私钥的文件名
+     * @param string $type     certType规定的私钥的加密类型,支持的密钥类型为”PEM”(默认值)、”DER”和”ENG”
+     * @param string $password SSL私钥的密码
+     *
+     * @return static
+     */
+    public function sslKey($path, $type = null, $password = null)
+    {
+        $this->keyPath = $path;
+        if (null !== $type)
+        {
+            $this->keyType = $type;
+        }
+        if (null !== $password)
+        {
+            $this->keyPassword = $password;
+        }
+
+        return $this;
+    }
+
+    /**
+     * 设置请求方法.
+     *
+     * @param string $method
+     *
+     * @return static
+     */
+    public function method($method)
+    {
+        $this->method = $method;
+
+        return $this;
+    }
+
+    /**
+     * 设置是否启用连接池.
+     *
+     * @param bool $connectionPool
+     *
+     * @return static
+     */
+    public function connectionPool($connectionPool)
+    {
+        $this->connectionPool = $connectionPool;
+
+        return $this;
+    }
+
+    /**
+     * 处理请求主体.
+     *
+     * @param string|string|array $requestBody
+     * @param string|null         $contentType 内容类型,支持null/json,为null时不处理
+     *
+     * @return array
+     */
+    protected function parseRequestBody($requestBody, $contentType)
+    {
+        $body = $files = [];
+        if (\is_string($requestBody))
+        {
+            $body = $requestBody;
+        }
+        elseif (\is_array($requestBody))
+        {
+            switch ($contentType)
+            {
+                case 'json':
+                    $body = json_encode($requestBody);
+                    $this->header('Content-Type', MediaType::APPLICATION_JSON);
+                    break;
+                default:
+                    foreach ($requestBody as $k => $v)
+                    {
+                        if ($v instanceof UploadedFile)
+                        {
+                            $files[$k] = $v;
+                        }
+                        else
+                        {
+                            $body[$k] = $v;
+                        }
+                    }
+                    $body = http_build_query($body, '', '&');
+            }
+        }
+        else
+        {
+            throw new \InvalidArgumentException('$requestBody only can be string or array');
+        }
+
+        return [$body, $files];
+    }
+
+    /**
+     * 构建请求类.
+     *
+     * @param string       $url         请求地址,如果为null则取url属性值
+     * @param string|array $requestBody 发送内容,可以是字符串、数组,如果为空则取content属性值
+     * @param string|null  $method      请求方法,GET、POST等
+     * @param string|null  $contentType 内容类型,支持null/json,为null时不处理
+     *
+     * @return \Yurun\Util\YurunHttp\Http\Request
+     */
+    public function buildRequest($url = null, $requestBody = null, $method = null, $contentType = null)
+    {
+        if (null === $url)
+        {
+            $url = $this->url;
+        }
+        if (null === $method)
+        {
+            $method = $this->method;
+        }
+        list($body, $files) = $this->parseRequestBody(null === $requestBody ? $this->content : $requestBody, $contentType);
+        $request = new Request($url, $this->headers, $body, $method);
+        $saveFileOption = $this->saveFileOption;
+        $request = $request->withUploadedFiles($files)
+                            ->withCookieParams($this->cookies)
+                            ->withAttribute(Attributes::MAX_REDIRECTS, $this->maxRedirects)
+                            ->withAttribute(Attributes::IS_VERIFY_CA, $this->isVerifyCA)
+                            ->withAttribute(Attributes::CA_CERT, $this->caCert)
+                            ->withAttribute(Attributes::CERT_PATH, $this->certPath)
+                            ->withAttribute(Attributes::CERT_PASSWORD, $this->certPassword)
+                            ->withAttribute(Attributes::CERT_TYPE, $this->certType)
+                            ->withAttribute(Attributes::KEY_PATH, $this->keyPath)
+                            ->withAttribute(Attributes::KEY_PASSWORD, $this->keyPassword)
+                            ->withAttribute(Attributes::KEY_TYPE, $this->keyType)
+                            ->withAttribute(Attributes::OPTIONS, $this->options)
+                            ->withAttribute(Attributes::SAVE_FILE_PATH, isset($saveFileOption['filePath']) ? $saveFileOption['filePath'] : null)
+                            ->withAttribute(Attributes::USE_PROXY, $this->useProxy)
+                            ->withAttribute(Attributes::USERNAME, $this->username)
+                            ->withAttribute(Attributes::PASSWORD, $this->password)
+                            ->withAttribute(Attributes::CONNECT_TIMEOUT, $this->connectTimeout)
+                            ->withAttribute(Attributes::TIMEOUT, $this->timeout)
+                            ->withAttribute(Attributes::DOWNLOAD_SPEED, $this->downloadSpeed)
+                            ->withAttribute(Attributes::UPLOAD_SPEED, $this->uploadSpeed)
+                            ->withAttribute(Attributes::FOLLOW_LOCATION, $this->followLocation)
+                            ->withAttribute(Attributes::CONNECTION_POOL, $this->connectionPool)
+                            ->withProtocolVersion($this->protocolVersion)
+                            ;
+        foreach ($this->proxy as $name => $value)
+        {
+            $request = $request->withAttribute('proxy.' . $name, $value);
+        }
+
+        return $request;
+    }
+
+    /**
+     * 发送请求,所有请求的老祖宗.
+     *
+     * @param string|null       $url         请求地址,如果为null则取url属性值
+     * @param string|array|null $requestBody 发送内容,可以是字符串、数组,如果为空则取content属性值
+     * @param string            $method      请求方法,GET、POST等
+     * @param string|null       $contentType 内容类型,支持null/json,为null时不处理
+     *
+     * @return \Yurun\Util\YurunHttp\Http\Response|null
+     */
+    public function send($url = null, $requestBody = null, $method = null, $contentType = null)
+    {
+        $request = $this->buildRequest($url, $requestBody, $method, $contentType);
+
+        return YurunHttp::send($request, $this->handler);
+    }
+
+    /**
+     * 发送 Http2 请求不调用 recv().
+     *
+     * @param string|null       $url         请求地址,如果为null则取url属性值
+     * @param string|array|null $requestBody 发送内容,可以是字符串、数组,如果为空则取content属性值
+     * @param string            $method      请求方法,GET、POST等
+     * @param string|null       $contentType 内容类型,支持null/json,为null时不处理
+     *
+     * @return \Yurun\Util\YurunHttp\Http\Response|null
+     */
+    public function sendHttp2WithoutRecv($url = null, $requestBody = null, $method = 'GET', $contentType = null)
+    {
+        $request = $this->buildRequest($url, $requestBody, $method, $contentType)
+                        ->withProtocolVersion('2.0')
+                        ->withAttribute(Attributes::HTTP2_NOT_RECV, true);
+
+        return YurunHttp::send($request, $this->handler);
+    }
+
+    /**
+     * GET请求
+     *
+     * @param string       $url         请求地址,如果为null则取url属性值
+     * @param string|array $requestBody 发送内容,可以是字符串、数组,如果为空则取content属性值
+     *
+     * @return \Yurun\Util\YurunHttp\Http\Response|null
+     */
+    public function get($url = null, $requestBody = null)
+    {
+        if (!empty($requestBody))
+        {
+            if (strpos($url, '?'))
+            {
+                $url .= '&';
+            }
+            else
+            {
+                $url .= '?';
+            }
+            $url .= http_build_query($requestBody, '', '&');
+        }
+
+        return $this->send($url, [], 'GET');
+    }
+
+    /**
+     * POST请求
+     *
+     * @param string       $url         请求地址,如果为null则取url属性值
+     * @param string|array $requestBody 发送内容,可以是字符串、数组,如果为空则取content属性值
+     * @param string|null  $contentType 内容类型,支持null/json,为null时不处理
+     *
+     * @return \Yurun\Util\YurunHttp\Http\Response|null
+     */
+    public function post($url = null, $requestBody = null, $contentType = null)
+    {
+        return $this->send($url, $requestBody, 'POST', $contentType);
+    }
+
+    /**
+     * HEAD请求
+     *
+     * @param string       $url         请求地址,如果为null则取url属性值
+     * @param string|array $requestBody 发送内容,可以是字符串、数组,如果为空则取content属性值
+     *
+     * @return \Yurun\Util\YurunHttp\Http\Response|null
+     */
+    public function head($url = null, $requestBody = null)
+    {
+        return $this->send($url, $requestBody, 'HEAD');
+    }
+
+    /**
+     * PUT请求
+     *
+     * @param string       $url         请求地址,如果为null则取url属性值
+     * @param string|array $requestBody 发送内容,可以是字符串、数组,如果为空则取content属性值
+     * @param string|null  $contentType 内容类型,支持null/json,为null时不处理
+     *
+     * @return \Yurun\Util\YurunHttp\Http\Response|null
+     */
+    public function put($url = null, $requestBody = null, $contentType = null)
+    {
+        return $this->send($url, $requestBody, 'PUT', $contentType);
+    }
+
+    /**
+     * PATCH请求
+     *
+     * @param string       $url         请求地址,如果为null则取url属性值
+     * @param string|array $requestBody 发送内容,可以是字符串、数组,如果为空则取content属性值
+     * @param string|null  $contentType 内容类型,支持null/json,为null时不处理
+     *
+     * @return \Yurun\Util\YurunHttp\Http\Response|null
+     */
+    public function patch($url = null, $requestBody = null, $contentType = null)
+    {
+        return $this->send($url, $requestBody, 'PATCH', $contentType);
+    }
+
+    /**
+     * DELETE请求
+     *
+     * @param string       $url         请求地址,如果为null则取url属性值
+     * @param string|array $requestBody 发送内容,可以是字符串、数组,如果为空则取content属性值
+     * @param string|null  $contentType 内容类型,支持null/json,为null时不处理
+     *
+     * @return \Yurun\Util\YurunHttp\Http\Response|null
+     */
+    public function delete($url = null, $requestBody = null, $contentType = null)
+    {
+        return $this->send($url, $requestBody, 'DELETE', $contentType);
+    }
+
+    /**
+     * 直接下载文件.
+     *
+     * @param string       $fileName    保存路径,如果以 .* 结尾,则根据 Content-Type 自动决定扩展名
+     * @param string       $url         下载文件地址
+     * @param string|array $requestBody 发送内容,可以是字符串、数组,如果为空则取content属性值
+     * @param string       $method      请求方法,GET、POST等,一般用GET
+     *
+     * @return \Yurun\Util\YurunHttp\Http\Response|null
+     */
+    public function download($fileName, $url = null, $requestBody = null, $method = 'GET')
+    {
+        $isAutoExt = self::checkDownloadIsAutoExt($fileName, $fileName);
+        $result = $this->saveFile($fileName)->send($url, $requestBody, $method);
+        if ($isAutoExt)
+        {
+            self::parseDownloadAutoExt($result, $fileName);
+        }
+        $this->saveFileOption = [];
+
+        return $result;
+    }
+
+    /**
+     * WebSocket.
+     *
+     * @param string $url
+     *
+     * @return \Yurun\Util\YurunHttp\WebSocket\IWebSocketClient
+     */
+    public function websocket($url = null)
+    {
+        $request = $this->buildRequest($url);
+
+        return YurunHttp::websocket($request, $this->handler);
+    }
+
+    /**
+     * 检查下载文件名是否要自动扩展名.
+     *
+     * @param string $fileName
+     * @param string $tempFileName
+     *
+     * @return bool
+     */
+    public static function checkDownloadIsAutoExt($fileName, &$tempFileName)
+    {
+        $flagLength = \strlen(self::AUTO_EXT_FLAG);
+        if (self::AUTO_EXT_FLAG !== substr($fileName, -$flagLength))
+        {
+            return false;
+        }
+        $tempFileName = substr($fileName, 0, -$flagLength) . self::AUTO_EXT_TEMP_EXT;
+
+        return true;
+    }
+
+    /**
+     * 处理下载的自动扩展名.
+     *
+     * @param \Yurun\Util\YurunHttp\Http\Response $response
+     * @param string                              $tempFileName
+     *
+     * @return void
+     */
+    public static function parseDownloadAutoExt(&$response, $tempFileName)
+    {
+        $ext = MediaType::getExt($response->getHeaderLine('Content-Type'));
+        if (null === $ext)
+        {
+            $ext = 'file';
+        }
+        $savedFileName = substr($tempFileName, 0, -\strlen(self::AUTO_EXT_TEMP_EXT)) . '.' . $ext;
+        rename($tempFileName, $savedFileName);
+        $response = $response->withSavedFileName($savedFileName);
+    }
+}
+
+if (\extension_loaded('curl'))
+{
+    // 代理认证方式
+    HttpRequest::$proxyAuths = [
+        'basic' => \CURLAUTH_BASIC,
+        'ntlm'  => \CURLAUTH_NTLM,
+    ];
+
+    // 代理类型
+    HttpRequest::$proxyType = [
+        'http'      => \CURLPROXY_HTTP,
+        'socks4'    => \CURLPROXY_SOCKS4,
+        'socks4a'   => 6,    // CURLPROXY_SOCKS4A
+        'socks5'    => \CURLPROXY_SOCKS5,
+    ];
+}

+ 200 - 0
vendor/yurunsoft/yurun-http/src/YurunHttp.php

@@ -0,0 +1,200 @@
+<?php
+
+namespace Yurun\Util;
+
+use Swoole\Coroutine;
+use Yurun\Util\YurunHttp\Handler\IHandler;
+
+abstract class YurunHttp
+{
+    /**
+     * 默认处理器类.
+     *
+     * @var string|null
+     */
+    private static $defaultHandler = null;
+
+    /**
+     * 属性.
+     *
+     * @var array
+     */
+    private static $attributes = [];
+
+    /**
+     * 版本号.
+     */
+    const VERSION = '4.3';
+
+    /**
+     * 设置默认处理器类.
+     *
+     * @param string|null $class
+     *
+     * @return void
+     */
+    public static function setDefaultHandler($class)
+    {
+        static::$defaultHandler = $class;
+    }
+
+    /**
+     * 获取默认处理器类.
+     *
+     * @return string|null
+     */
+    public static function getDefaultHandler()
+    {
+        return static::$defaultHandler;
+    }
+
+    /**
+     * 获取处理器类.
+     *
+     * @return \Yurun\Util\YurunHttp\Handler\IHandler
+     */
+    public static function getHandler()
+    {
+        if (static::$defaultHandler)
+        {
+            $class = static::$defaultHandler;
+        }
+        elseif (\defined('SWOOLE_VERSION') && Coroutine::getuid() > -1)
+        {
+            $class = \Yurun\Util\YurunHttp\Handler\Swoole::class;
+        }
+        else
+        {
+            $class = \Yurun\Util\YurunHttp\Handler\Curl::class;
+        }
+
+        return new $class();
+    }
+
+    /**
+     * 发送请求并获取结果.
+     *
+     * @param \Yurun\Util\YurunHttp\Http\Request                 $request
+     * @param \Yurun\Util\YurunHttp\Handler\IHandler|string|null $handlerClass
+     *
+     * @return \Yurun\Util\YurunHttp\Http\Response|null
+     */
+    public static function send($request, $handlerClass = null)
+    {
+        if ($handlerClass instanceof IHandler)
+        {
+            $handler = $handlerClass;
+            $needClose = false;
+        }
+        else
+        {
+            $needClose = true;
+            if (null === $handlerClass)
+            {
+                $handler = static::getHandler();
+            }
+            else
+            {
+                $handler = new $handlerClass();
+            }
+        }
+        /** @var IHandler $handler */
+        $time = microtime(true);
+        foreach (static::$attributes as $name => $value)
+        {
+            if (null === $request->getAttribute($name))
+            {
+                $request = $request->withAttribute($name, $value);
+            }
+        }
+        $handler->send($request);
+        $response = $handler->recv();
+        if (!$response)
+        {
+            return $response;
+        }
+        $response = $response->withTotalTime(microtime(true) - $time);
+        if ($needClose)
+        {
+            $handler->close();
+        }
+
+        return $response;
+    }
+
+    /**
+     * 发起 WebSocket 连接.
+     *
+     * @param \Yurun\Util\YurunHttp\Http\Request            $request
+     * @param \Yurun\Util\YurunHttp\Handler\IHandler|string $handlerClass
+     *
+     * @return \Yurun\Util\YurunHttp\WebSocket\IWebSocketClient
+     */
+    public static function websocket($request, $handlerClass = null)
+    {
+        if ($handlerClass instanceof IHandler)
+        {
+            $handler = $handlerClass;
+        }
+        elseif (null === $handlerClass)
+        {
+            $handler = static::getHandler();
+        }
+        else
+        {
+            $handler = new $handlerClass();
+        }
+        foreach (static::$attributes as $name => $value)
+        {
+            if (null === $request->getAttribute($name))
+            {
+                $request = $request->withAttribute($name, $value);
+            }
+        }
+
+        return $handler->websocket($request);
+    }
+
+    /**
+     * 获取所有全局属性.
+     *
+     * @return array
+     */
+    public static function getAttributes()
+    {
+        return static::$attributes;
+    }
+
+    /**
+     * 获取全局属性值
+     *
+     * @param string $name
+     * @param mixed  $default
+     *
+     * @return mixed
+     */
+    public static function getAttribute($name, $default = null)
+    {
+        if (\array_key_exists($name, static::$attributes))
+        {
+            return static::$attributes[$name];
+        }
+        else
+        {
+            return $default;
+        }
+    }
+
+    /**
+     * 设置全局属性值
+     *
+     * @param string $name
+     * @param mixed  $value
+     *
+     * @return mixed
+     */
+    public static function setAttribute($name, $value)
+    {
+        static::$attributes[$name] = $value;
+    }
+}

+ 216 - 0
vendor/yurunsoft/yurun-http/src/YurunHttp/Attributes.php

@@ -0,0 +1,216 @@
+<?php
+
+namespace Yurun\Util\YurunHttp;
+
+/**
+ * 所有属性的常量定义.
+ *
+ * PRIVATE_ 开头的为内部属性,请勿使用
+ */
+abstract class Attributes
+{
+    /**
+     * 客户端参数.
+     */
+    const OPTIONS = 'options';
+
+    /**
+     * 全局默认 UserAgent.
+     */
+    const USER_AGENT = 'userAgent';
+
+    /**
+     * 重试次数.
+     */
+    const RETRY = 'retry';
+
+    /**
+     * 下载文件保存路径.
+     */
+    const SAVE_FILE_PATH = 'saveFilePath';
+
+    /**
+     * 保存文件的模型.
+     */
+    const SAVE_FILE_MODE = 'saveFileMode';
+
+    /**
+     * 允许重定向.
+     */
+    const FOLLOW_LOCATION = 'followLocation';
+
+    /**
+     * 最大允许重定向次数.
+     */
+    const MAX_REDIRECTS = 'maxRedirects';
+
+    /**
+     * 是否验证 CA 证书.
+     */
+    const IS_VERIFY_CA = 'isVerifyCA';
+
+    /**
+     * CA 证书.
+     */
+    const CA_CERT = 'caCert';
+
+    /**
+     * SSL 证书类型.
+     */
+    const CERT_TYPE = 'certType';
+
+    /**
+     * SSL 证书路径.
+     */
+    const CERT_PATH = 'certPath';
+
+    /**
+     * SSL 证书密码
+     */
+    const CERT_PASSWORD = 'certPassword';
+
+    /**
+     * SSL 密钥类型.
+     */
+    const KEY_TYPE = 'keyType';
+
+    /**
+     * SSL 密钥路径.
+     */
+    const KEY_PATH = 'keyPath';
+
+    /**
+     * SSL 密钥密码
+     */
+    const KEY_PASSWORD = 'keyPassword';
+
+    /**
+     * 使用代理.
+     */
+    const USE_PROXY = 'useProxy';
+
+    /**
+     * 代理类型.
+     */
+    const PROXY_TYPE = 'proxy.type';
+
+    /**
+     * 代理服务器地址
+     */
+    const PROXY_SERVER = 'proxy.server';
+
+    /**
+     * 代理服务器端口.
+     */
+    const PROXY_PORT = 'proxy.port';
+
+    /**
+     * 代理用户名.
+     */
+    const PROXY_USERNAME = 'proxy.username';
+
+    /**
+     * 代理密码
+     */
+    const PROXY_PASSWORD = 'proxy.password';
+
+    /**
+     * 代理的 Basic 认证配置.
+     */
+    const PROXY_AUTH = 'proxy.auth';
+
+    /**
+     * 认证用户名.
+     */
+    const USERNAME = 'username';
+
+    /**
+     * 认证密码
+     */
+    const PASSWORD = 'password';
+
+    /**
+     * 超时时间.
+     */
+    const TIMEOUT = 'timeout';
+
+    /**
+     * 连接超时.
+     */
+    const CONNECT_TIMEOUT = 'connectTimeout';
+
+    /**
+     * 保持长连接.
+     */
+    const KEEP_ALIVE = 'keep_alive';
+
+    /**
+     * 下载限速
+     */
+    const DOWNLOAD_SPEED = 'downloadSpeed';
+
+    /**
+     * 上传限速
+     */
+    const UPLOAD_SPEED = 'uploadSpeed';
+
+    /**
+     * 使用自定义重定向操作.
+     */
+    const CUSTOM_LOCATION = 'customLocation';
+
+    /**
+     * http2 请求不调用 recv().
+     */
+    const HTTP2_NOT_RECV = 'http2_not_recv';
+
+    /**
+     * 启用 Http2 pipeline.
+     */
+    const HTTP2_PIPELINE = 'http2_pipeline';
+
+    /**
+     * 启用连接池.
+     */
+    const CONNECTION_POOL = 'connection_pool';
+
+    /**
+     * 重试计数.
+     */
+    const PRIVATE_RETRY_COUNT = '__retryCount';
+
+    /**
+     * 重定向计数.
+     */
+    const PRIVATE_REDIRECT_COUNT = '__redirectCount';
+
+    /**
+     * WebSocket 请求
+     */
+    const PRIVATE_WEBSOCKET = '__websocket';
+
+    /**
+     * Http2 流ID.
+     */
+    const PRIVATE_HTTP2_STREAM_ID = '__http2StreamId';
+
+    /**
+     * 是否为 Http2.
+     */
+    const PRIVATE_IS_HTTP2 = '__isHttp2';
+
+    /**
+     * 是否为 WebSocket.
+     */
+    const PRIVATE_IS_WEBSOCKET = '__isWebSocket';
+
+    /**
+     * 连接对象
+     */
+    const PRIVATE_CONNECTION = '__connection';
+
+    /**
+     * 连接池的键.
+     */
+    const PRIVATE_POOL_KEY = '__poolKey';
+}

+ 70 - 0
vendor/yurunsoft/yurun-http/src/YurunHttp/Co/Batch.php

@@ -0,0 +1,70 @@
+<?php
+
+namespace Yurun\Util\YurunHttp\Co;
+
+use Yurun\Util\HttpRequest;
+use Yurun\Util\YurunHttp;
+use Yurun\Util\YurunHttp\Attributes;
+
+abstract class Batch
+{
+    /**
+     * 批量运行并发请求
+     *
+     * @param \Yurun\Util\YurunHttp\Http\Request[]|\Yurun\Util\HttpRequest[] $requests
+     * @param float|null                                                     $timeout      超时时间,单位:秒。默认为 null 不限制
+     * @param string|null                                                    $handlerClass
+     *
+     * @return \Yurun\Util\YurunHttp\Http\Response[]
+     */
+    public static function run($requests, $timeout = null, $handlerClass = null)
+    {
+        $batchRequests = [];
+        $downloadAutoExt = [];
+        foreach ($requests as $i => $request)
+        {
+            if ($request instanceof HttpRequest)
+            {
+                $savePath = $request->getSavePath();
+                if (null !== $savePath && HttpRequest::checkDownloadIsAutoExt($savePath, $savePath))
+                {
+                    $request->saveFileOption['filePath'] = $savePath;
+                    $downloadAutoExt[] = $i;
+                }
+                $batchRequests[$i] = $request->buildRequest();
+            }
+            elseif (!$request instanceof \Yurun\Util\YurunHttp\Http\Request)
+            {
+                throw new \InvalidArgumentException('Request must be instance of \Yurun\Util\YurunHttp\Http\Request or \Yurun\Util\HttpRequest');
+            }
+        }
+        if (null === $handlerClass)
+        {
+            $handler = YurunHttp::getHandler();
+        }
+        else
+        {
+            $handler = new $handlerClass();
+        }
+        /** @var \Yurun\Util\YurunHttp\Handler\IHandler $handler */
+        $result = $handler->coBatch($batchRequests, $timeout);
+        foreach ($downloadAutoExt as $i)
+        {
+            if (isset($result[$i]))
+            {
+                $response = &$result[$i];
+            }
+            else
+            {
+                $response = null;
+            }
+            if ($response)
+            {
+                HttpRequest::parseDownloadAutoExt($response, $response->getRequest()->getAttribute(Attributes::SAVE_FILE_PATH));
+            }
+            unset($response);
+        }
+
+        return $result;
+    }
+}

+ 169 - 0
vendor/yurunsoft/yurun-http/src/YurunHttp/ConnectionPool.php

@@ -0,0 +1,169 @@
+<?php
+
+namespace Yurun\Util\YurunHttp;
+
+use Psr\Http\Message\UriInterface;
+use Yurun\Util\YurunHttp\Handler\Contract\IConnectionManager;
+use Yurun\Util\YurunHttp\Handler\Curl\CurlHttpConnectionManager;
+use Yurun\Util\YurunHttp\Handler\Swoole\SwooleHttpConnectionManager;
+use Yurun\Util\YurunHttp\Http\Psr7\Uri;
+use Yurun\Util\YurunHttp\Pool\Config\PoolConfig;
+
+class ConnectionPool
+{
+    /**
+     * 是否启用连接池.
+     *
+     * @var bool
+     */
+    private static $enabled = false;
+
+    /**
+     * 连接池配置集合.
+     *
+     * @var PoolConfig[]
+     */
+    private static $connectionPoolConfigs = [];
+
+    /**
+     * 连接管理类列表.
+     *
+     * @var array
+     */
+    private static $connectionManagers = [
+        CurlHttpConnectionManager::class,
+        SwooleHttpConnectionManager::class,
+    ];
+
+    private function __construct()
+    {
+    }
+
+    /**
+     * Get 是否启用连接池.
+     *
+     * @return bool
+     */
+    public static function isEnabled()
+    {
+        return self::$enabled;
+    }
+
+    /**
+     * 启用连接池.
+     *
+     * @return void
+     */
+    public static function enable()
+    {
+        self::$enabled = true;
+    }
+
+    /**
+     * 禁用连接池.
+     *
+     * @return void
+     */
+    public static function disable()
+    {
+        self::$enabled = false;
+    }
+
+    /**
+     * 设置连接池配置.
+     *
+     * @param string $url
+     * @param int    $maxConnections
+     * @param int    $waitTimeout
+     *
+     * @return void
+     */
+    public static function setConfig($url, $maxConnections = 0, $waitTimeout = 30)
+    {
+        if (isset(self::$connectionPoolConfigs[$url]))
+        {
+            $config = self::$connectionPoolConfigs[$url];
+            $config->setMaxConnections($maxConnections);
+            $config->setWaitTimeout($waitTimeout);
+        }
+        else
+        {
+            self::$connectionPoolConfigs[$url] = $config = new PoolConfig($url, $maxConnections, $waitTimeout);
+        }
+        foreach (self::$connectionManagers as $class)
+        {
+            /** @var IConnectionManager $connectionManager */
+            $connectionManager = $class::getInstance();
+            $connectionManagerConfig = $connectionManager->getConfig($url);
+            if ($connectionManagerConfig)
+            {
+                $connectionManagerConfig->setMaxConnections($maxConnections);
+                $connectionManagerConfig->setWaitTimeout($waitTimeout);
+            }
+            else
+            {
+                $connectionManager->setConfig($url, $maxConnections, $waitTimeout);
+            }
+        }
+    }
+
+    /**
+     * 获取连接池配置.
+     *
+     * @param string $url
+     *
+     * @return PoolConfig|null
+     */
+    public static function getConfig($url)
+    {
+        if (isset(self::$connectionPoolConfigs[$url]))
+        {
+            return self::$connectionPoolConfigs[$url];
+        }
+        else
+        {
+            return null;
+        }
+    }
+
+    /**
+     * 获取键.
+     *
+     * @param string|UriInterface $url
+     *
+     * @return string
+     */
+    public static function getKey($url)
+    {
+        if ($url instanceof UriInterface)
+        {
+            return $url->getScheme() . '://' . Uri::getDomain($url);
+        }
+        else
+        {
+            return $url;
+        }
+    }
+
+    /**
+     * Get 连接管理类列表.
+     *
+     * @return array
+     */
+    public static function getConnectionManagers()
+    {
+        return self::$connectionManagers;
+    }
+
+    /**
+     * Set 连接管理类列表.
+     *
+     * @param array $connectionManagers 连接管理类列表
+     *
+     * @return void
+     */
+    public static function setConnectionManagers(array $connectionManagers)
+    {
+        self::$connectionManagers = $connectionManagers;
+    }
+}

+ 144 - 0
vendor/yurunsoft/yurun-http/src/YurunHttp/Cookie/CookieItem.php

@@ -0,0 +1,144 @@
+<?php
+
+namespace Yurun\Util\YurunHttp\Cookie;
+
+/**
+ * Cookie 项.
+ */
+class CookieItem
+{
+    /**
+     * 名称.
+     *
+     * @var string
+     */
+    public $name;
+
+    /**
+     * 值
+     *
+     * @var string
+     */
+    public $value;
+
+    /**
+     * 过期时间戳.
+     *
+     * @var int
+     */
+    public $expires = 0;
+
+    /**
+     * 路径.
+     *
+     * @var string
+     */
+    public $path = '/';
+
+    /**
+     * 域名.
+     *
+     * @var string
+     */
+    public $domain = '';
+
+    /**
+     * 是否 https.
+     *
+     * @var bool
+     */
+    public $secure = false;
+
+    /**
+     * 是否禁止 js 操作该 Cookie.
+     *
+     * @var bool
+     */
+    public $httpOnly = false;
+
+    /**
+     * @param string $name
+     * @param string $value
+     * @param int    $expires
+     * @param string $path
+     * @param string $domain
+     * @param bool   $secure
+     * @param bool   $httpOnly
+     */
+    public function __construct($name, $value, $expires = 0, $path = '/', $domain = '', $secure = false, $httpOnly = false)
+    {
+        $this->name = $name;
+        $this->value = $value;
+        $this->expires = (int) $expires;
+        $this->path = $path;
+        $this->domain = $domain;
+        $this->secure = $secure;
+        $this->httpOnly = $httpOnly;
+    }
+
+    /**
+     * 获取新实例对象
+     *
+     * @param array $data
+     *
+     * @return static
+     */
+    public static function newInstance($data)
+    {
+        $object = new static('', '');
+        foreach ($data as $k => $v)
+        {
+            $object->$k = $v;
+        }
+
+        return $object;
+    }
+
+    /**
+     * 从 Set-Cookie 中解析.
+     *
+     * @param string $setCookieContent
+     *
+     * @return static|null
+     */
+    public static function fromSetCookie($setCookieContent)
+    {
+        if (preg_match_all('/;?\s*((?P<name>[^=;]+)=(?P<value>[^;]+)|((?P<name2>[^=;]+)))/', $setCookieContent, $matches) > 0)
+        {
+            $name = $matches['name'][0];
+            $value = $matches['value'][0];
+            unset($matches['name'][0], $matches['value'][0]);
+            $data = array_combine(array_map('strtolower', $matches['name']), $matches['value']);
+            if (isset($data['']))
+            {
+                unset($data['']);
+            }
+            if (isset($data['max-age']))
+            {
+                $expires = time() + $data['max-age'];
+            }
+            elseif (isset($data['expires']))
+            {
+                $expires = strtotime($data['expires']);
+            }
+            else
+            {
+                $expires = null;
+            }
+            foreach ($matches['name2'] as $boolItemName)
+            {
+                if ('' !== $boolItemName)
+                {
+                    $data[strtolower($boolItemName)] = true;
+                }
+            }
+            $object = new static($name, $value, $expires, isset($data['path']) ? $data['path'] : '/', isset($data['domain']) ? $data['domain'] : '', isset($data['secure']) ? $data['secure'] : false, isset($data['httponly']) ? $data['httponly'] : false);
+
+            return $object;
+        }
+        else
+        {
+            return null;
+        }
+    }
+}

+ 339 - 0
vendor/yurunsoft/yurun-http/src/YurunHttp/Cookie/CookieManager.php

@@ -0,0 +1,339 @@
+<?php
+
+namespace Yurun\Util\YurunHttp\Cookie;
+
+use Yurun\Util\YurunHttp\Http\Psr7\Uri;
+
+class CookieManager
+{
+    /**
+     * Cookie 列表.
+     *
+     * @var \Yurun\Util\YurunHttp\Cookie\CookieItem[]
+     */
+    protected $cookieList;
+
+    /**
+     * 关联集合.
+     *
+     * @var array
+     */
+    protected $relationMap;
+
+    /**
+     * 自增ID
+     * 会比当前列表长度+1.
+     *
+     * @var int
+     */
+    protected $autoIncrementId;
+
+    /**
+     * __construct.
+     *
+     * @param array $cookieList
+     */
+    public function __construct($cookieList = [])
+    {
+        $this->setCookieList($cookieList);
+    }
+
+    /**
+     * 设置 Cookie 列表.
+     *
+     * @param array $cookieList
+     *
+     * @return void
+     */
+    public function setCookieList($cookieList)
+    {
+        $this->autoIncrementId = 1;
+        $this->cookieList = [];
+        $this->relationMap = [];
+        foreach ($cookieList as $item)
+        {
+            $item = CookieItem::newInstance($item);
+            $this->insertCookie($item);
+        }
+    }
+
+    /**
+     * 获取 Cookie 列表.
+     *
+     * @return array
+     */
+    public function getCookieList()
+    {
+        return $this->cookieList;
+    }
+
+    /**
+     * 添加 Set-Cookie.
+     *
+     * @param string $setCookie
+     *
+     * @return \Yurun\Util\YurunHttp\Cookie\CookieItem
+     */
+    public function addSetCookie($setCookie)
+    {
+        $item = CookieItem::fromSetCookie($setCookie);
+        if (($id = $this->findCookie($item)) > 0)
+        {
+            $this->updateCookie($id, $item);
+        }
+        else
+        {
+            $this->insertCookie($item);
+        }
+
+        return $item;
+    }
+
+    /**
+     * 设置 Cookie.
+     *
+     * @param string $name
+     * @param string $value
+     * @param int    $expires
+     * @param string $path
+     * @param string $domain
+     * @param bool   $secure
+     * @param bool   $httpOnly
+     *
+     * @return \Yurun\Util\YurunHttp\Cookie\CookieItem
+     */
+    public function setCookie($name, $value, $expires = 0, $path = '/', $domain = '', $secure = false, $httpOnly = false)
+    {
+        $item = new CookieItem($name, $value, $expires, $path, $domain, $secure, $httpOnly);
+        if (($id = $this->findCookie($item)) > 0)
+        {
+            $this->updateCookie($id, $item);
+        }
+        else
+        {
+            $this->insertCookie($item);
+        }
+
+        return $item;
+    }
+
+    /**
+     * Cookie 数量.
+     *
+     * @return int
+     */
+    public function count()
+    {
+        return \count($this->cookieList);
+    }
+
+    /**
+     * 获取请求所需 Cookie 关联数组.
+     *
+     * @param \Psr\Http\Message\UriInterface $uri
+     *
+     * @return array
+     */
+    public function getRequestCookies($uri)
+    {
+        // @phpstan-ignore-next-line
+        if (\defined('SWOOLE_VERSION') && \SWOOLE_VERSION < 4.4)
+        {
+            // Fix bug: https://github.com/swoole/swoole-src/pull/2644
+            $result = json_decode('[]', true);
+        }
+        else
+        {
+            $result = [];
+        }
+        $uriDomain = Uri::getDomain($uri);
+        $uriPath = $uri->getPath();
+        $cookieList = &$this->cookieList;
+        foreach ($this->relationMap as $relationDomain => $list1)
+        {
+            if ('' === $relationDomain || $this->checkDomain($uriDomain, $relationDomain))
+            {
+                foreach ($list1 as $path => $idList)
+                {
+                    if ($this->checkPath($uriPath, $path))
+                    {
+                        foreach ($idList as $id)
+                        {
+                            $cookieItem = $cookieList[$id];
+                            if ((0 === $cookieItem->expires || $cookieItem->expires > time()) && (!$cookieItem->secure || 'https' === $uri->getScheme() || 'wss' === $uri->getScheme()))
+                            {
+                                $result[$cookieItem->name] = $cookieItem->value;
+                            }
+                        }
+                    }
+                }
+            }
+        }
+
+        return $result;
+    }
+
+    /**
+     * 获取请求所需 Cookie 关联数组.
+     *
+     * @param \Psr\Http\Message\UriInterface $uri
+     *
+     * @return string
+     */
+    public function getRequestCookieString($uri)
+    {
+        $content = '';
+        foreach ($this->getRequestCookies($uri) as $name => $value)
+        {
+            $content .= "{$name}={$value}; ";
+        }
+
+        return $content;
+    }
+
+    /**
+     * 获取 CookieItem.
+     *
+     * @param string $name
+     * @param string $domain
+     * @param string $path
+     *
+     * @return \Yurun\Util\YurunHttp\Cookie\CookieItem|null
+     */
+    public function getCookieItem($name, $domain = '', $path = '/')
+    {
+        if (isset($this->relationMap[$domain][$path][$name]))
+        {
+            $id = $this->relationMap[$domain][$path][$name];
+
+            return $this->cookieList[$id];
+        }
+
+        return null;
+    }
+
+    /**
+     * 检查 uri 域名和 cookie 域名.
+     *
+     * @param string $uriDomain
+     * @param string $cookieDomain
+     *
+     * @return bool
+     */
+    private function checkDomain($uriDomain, $cookieDomain)
+    {
+        return ($uriDomain === $cookieDomain)
+                || (isset($cookieDomain[0]) && '.' === $cookieDomain[0] && substr($uriDomain, -\strlen($cookieDomain) - 1) === '.' . $cookieDomain)
+                ;
+    }
+
+    /**
+     * 检查 uri 路径和 cookie 路径.
+     *
+     * @param string $uriPath
+     * @param string $cookiePath
+     *
+     * @return bool
+     */
+    private function checkPath($uriPath, $cookiePath)
+    {
+        $uriPath = rtrim($uriPath, '/');
+        $cookiePath = rtrim($cookiePath, '/');
+        if ($uriPath === $cookiePath)
+        {
+            return true;
+        }
+        $uriPathDSCount = substr_count($uriPath, '/');
+        $cookiePathDSCount = substr_count($cookiePath, '/');
+        if ('' === $uriPath)
+        {
+            $uriPath = '/';
+        }
+        if ('' === $cookiePath)
+        {
+            $cookiePath = '/';
+        }
+        if ($uriPathDSCount > $cookiePathDSCount)
+        {
+            if (version_compare(\PHP_VERSION, '7.0', '>='))
+            {
+                $path = \dirname($uriPath, $uriPathDSCount - $cookiePathDSCount);
+            }
+            else
+            {
+                $count = $uriPathDSCount - $cookiePathDSCount;
+                $path = $uriPath;
+                while ($count--)
+                {
+                    $path = \dirname($path);
+                }
+            }
+            if ('\\' === \DIRECTORY_SEPARATOR && false !== strpos($path, \DIRECTORY_SEPARATOR))
+            {
+                $path = str_replace(\DIRECTORY_SEPARATOR, '/', $path);
+            }
+
+            return $path === $cookiePath;
+        }
+        else
+        {
+            return false;
+        }
+    }
+
+    /**
+     * 更新 Cookie 数据.
+     *
+     * @param int                                     $id
+     * @param \Yurun\Util\YurunHttp\Cookie\CookieItem $item
+     *
+     * @return void
+     */
+    private function updateCookie($id, $item)
+    {
+        if (isset($this->cookieList[$id]))
+        {
+            $object = $this->cookieList[$id];
+            // @phpstan-ignore-next-line
+            foreach ($item as $k => $v)
+            {
+                $object->$k = $v;
+            }
+        }
+    }
+
+    /**
+     * 插入 Cookie 数据.
+     *
+     * @param \Yurun\Util\YurunHttp\Cookie\CookieItem $item
+     *
+     * @return int
+     */
+    private function insertCookie($item)
+    {
+        $id = $this->autoIncrementId++;
+        $this->cookieList[$id] = $item;
+        $this->relationMap[$item->domain][$item->path][$item->name] = $id;
+
+        return $id;
+    }
+
+    /**
+     * 查找 Cookie ID.
+     *
+     * @param \Yurun\Util\YurunHttp\Cookie\CookieItem $item
+     *
+     * @return int|null
+     */
+    private function findCookie($item)
+    {
+        if (isset($this->relationMap[$item->domain][$item->path][$item->name]))
+        {
+            return $this->relationMap[$item->domain][$item->path][$item->name];
+        }
+        else
+        {
+            return null;
+        }
+    }
+}

+ 7 - 0
vendor/yurunsoft/yurun-http/src/YurunHttp/Exception/WebSocketException.php

@@ -0,0 +1,7 @@
+<?php
+
+namespace Yurun\Util\YurunHttp\Exception;
+
+class WebSocketException extends \Exception
+{
+}

+ 36 - 0
vendor/yurunsoft/yurun-http/src/YurunHttp/FormDataBuilder.php

@@ -0,0 +1,36 @@
+<?php
+
+namespace Yurun\Util\YurunHttp;
+
+abstract class FormDataBuilder
+{
+    /**
+     * 构建form-data body内容.
+     *
+     * @param mixed                                          $body
+     * @param \Yurun\Util\YurunHttp\Http\Psr7\UploadedFile[] $files
+     * @param string                                         $boundary
+     *
+     * @return string
+     */
+    public static function build($body, $files, &$boundary)
+    {
+        $result = '';
+        if (!\is_array($body))
+        {
+            parse_str($body, $body);
+        }
+        $boundary = Random::letter(8, 16);
+        foreach ($body as $k => $v)
+        {
+            $result .= sprintf("--%s\r\nContent-Disposition: form-data; name=\"%s\"\r\n\r\n%s\r\n", $boundary, $k, $v);
+        }
+        foreach ($files as $name => $file)
+        {
+            $result .= sprintf("--%s\r\nContent-Disposition: form-data; name=\"%s\"; filename=\"%s\"\r\nContent-Type: %s\r\n\r\n", $boundary, $name, basename($file->getClientFilename()), $file->getClientMediaType()) . $file->getStream()->getContents() . "\r\n";
+        }
+        $result .= sprintf("--%s--\r\n", $boundary);
+
+        return $result;
+    }
+}

+ 89 - 0
vendor/yurunsoft/yurun-http/src/YurunHttp/Handler/Contract/IConnectionManager.php

@@ -0,0 +1,89 @@
+<?php
+
+namespace Yurun\Util\YurunHttp\Handler\Contract;
+
+use Yurun\Util\YurunHttp\Pool\Config\PoolConfig;
+use Yurun\Util\YurunHttp\Pool\Contract\IConnectionPool;
+
+interface IConnectionManager
+{
+    /**
+     * 获取连接池数组.
+     *
+     * @return IConnectionPool[]
+     */
+    public function getConnectionPools();
+
+    /**
+     * 获取连接池对象
+     *
+     * @param string $url
+     *
+     * @return IConnectionPool
+     */
+    public function getConnectionPool($url);
+
+    /**
+     * 获取连接.
+     *
+     * @param string $url
+     *
+     * @return mixed
+     */
+    public function getConnection($url);
+
+    /**
+     * 释放连接占用.
+     *
+     * @param string $url
+     * @param mixed  $connection
+     *
+     * @return void
+     */
+    public function release($url, $connection);
+
+    /**
+     * 关闭指定连接.
+     *
+     * @param string $url
+     *
+     * @return bool
+     */
+    public function closeConnection($url);
+
+    /**
+     * 创建新连接,但不归本管理器管理.
+     *
+     * @param string $url
+     *
+     * @return mixed
+     */
+    public function createConnection($url);
+
+    /**
+     * 关闭连接管理器.
+     *
+     * @return void
+     */
+    public function close();
+
+    /**
+     * 设置连接池配置.
+     *
+     * @param string $url
+     * @param int    $maxConnections
+     * @param int    $waitTimeout
+     *
+     * @return PoolConfig
+     */
+    public function setConfig($url, $maxConnections = 0, $waitTimeout = 30);
+
+    /**
+     * 获取连接池配置.
+     *
+     * @param string $url
+     *
+     * @return PoolConfig|null
+     */
+    public function getConfig($url);
+}

+ 798 - 0
vendor/yurunsoft/yurun-http/src/YurunHttp/Handler/Curl.php

@@ -0,0 +1,798 @@
+<?php
+
+namespace Yurun\Util\YurunHttp\Handler;
+
+use Psr\Http\Message\UriInterface;
+use Yurun\Util\YurunHttp;
+use Yurun\Util\YurunHttp\Attributes;
+use Yurun\Util\YurunHttp\ConnectionPool;
+use Yurun\Util\YurunHttp\FormDataBuilder;
+use Yurun\Util\YurunHttp\Handler\Curl\CurlHttpConnectionManager;
+use Yurun\Util\YurunHttp\Http\Psr7\Consts\MediaType;
+use Yurun\Util\YurunHttp\Http\Response;
+use Yurun\Util\YurunHttp\Stream\MemoryStream;
+use Yurun\Util\YurunHttp\Traits\TCookieManager;
+use Yurun\Util\YurunHttp\Traits\THandler;
+
+class Curl implements IHandler
+{
+    use TCookieManager;
+    use THandler;
+
+    /**
+     * 请求结果.
+     *
+     * @var \Yurun\Util\YurunHttp\Http\Response
+     */
+    private $result;
+
+    /**
+     * curl 句柄.
+     *
+     * @var resource|null
+     */
+    private $handler;
+
+    /**
+     * 请求内容.
+     *
+     * @var \Yurun\Util\YurunHttp\Http\Request
+     */
+    private $request;
+
+    /**
+     * 连接池键.
+     *
+     * @var string
+     */
+    private $poolKey;
+
+    /**
+     * 连接池是否启用.
+     *
+     * @var bool
+     */
+    private $poolIsEnabled = false;
+
+    /**
+     * 代理认证方式.
+     *
+     * @var array
+     */
+    public static $proxyAuths = [
+        'basic' => \CURLAUTH_BASIC,
+        'ntlm'  => \CURLAUTH_NTLM,
+    ];
+
+    /**
+     * 代理类型.
+     *
+     * @var array
+     */
+    public static $proxyType = [
+        'http'      => \CURLPROXY_HTTP,
+        'socks4'    => \CURLPROXY_SOCKS4,
+        'socks4a'   => 6, // CURLPROXY_SOCKS4A
+        'socks5'    => \CURLPROXY_SOCKS5,
+    ];
+
+    /**
+     * 本 Handler 默认的 User-Agent.
+     *
+     * @var string
+     */
+    private static $defaultUA;
+
+    public function __construct()
+    {
+        if (null === static::$defaultUA)
+        {
+            $version = curl_version();
+            static::$defaultUA = sprintf('Mozilla/5.0 YurunHttp/%s Curl/%s', YurunHttp::VERSION, isset($version['version']) ? $version['version'] : 'unknown');
+        }
+        $this->initCookieManager();
+    }
+
+    /**
+     * 关闭并释放所有资源.
+     *
+     * @return void
+     */
+    public function close()
+    {
+        if ($this->handler)
+        {
+            if ($this->poolIsEnabled)
+            {
+                CurlHttpConnectionManager::getInstance()->release($this->poolKey, $this->handler);
+            }
+            else
+            {
+                curl_close($this->handler);
+            }
+            $this->handler = null;
+        }
+    }
+
+    /**
+     * 发送请求
+     *
+     * @param \Yurun\Util\YurunHttp\Http\Request $request
+     *
+     * @return void
+     */
+    public function send(&$request)
+    {
+        $this->poolIsEnabled = $poolIsEnabled = ConnectionPool::isEnabled() && false !== $request->getAttribute(Attributes::CONNECTION_POOL);
+        if ($poolIsEnabled)
+        {
+            $httpConnectionManager = CurlHttpConnectionManager::getInstance();
+        }
+        try
+        {
+            $this->request = $request;
+            $request = &$this->request;
+            $handler = &$this->handler;
+            if (!$handler)
+            {
+                if ($poolIsEnabled)
+                {
+                    $this->poolKey = $poolKey = ConnectionPool::getKey($request->getUri());
+                    $handler = $httpConnectionManager->getConnection($poolKey);
+                }
+                else
+                {
+                    $handler = curl_init();
+                }
+            }
+            $files = $request->getUploadedFiles();
+            $body = (string) $request->getBody();
+
+            if (!empty($files))
+            {
+                $body = FormDataBuilder::build($body, $files, $boundary);
+                $request = $request->withHeader('Content-Type', MediaType::MULTIPART_FORM_DATA . '; boundary=' . $boundary);
+            }
+            $this->buildCurlHandlerBase($request, $handler, $receiveHeaders, $saveFileFp);
+            if ([] !== ($queryParams = $request->getQueryParams()))
+            {
+                $request = $request->withUri($request->getUri()->withQuery(http_build_query($queryParams, '', '&')));
+            }
+            $uri = $request->getUri();
+            $isLocation = false;
+            $statusCode = 0;
+            $redirectCount = 0;
+            do
+            {
+                // 请求方法
+                if ($isLocation && \in_array($statusCode, [301, 302, 303]))
+                {
+                    $method = 'GET';
+                }
+                else
+                {
+                    $method = $request->getMethod();
+                }
+                if ('GET' !== $method)
+                {
+                    $bodyContent = $body;
+                }
+                else
+                {
+                    $bodyContent = false;
+                }
+                $this->buildCurlHandlerEx($request, $handler, $uri, $method, $bodyContent);
+                $retry = $request->getAttribute(Attributes::RETRY, 0);
+                $result = null;
+                for ($i = 0; $i <= $retry; ++$i)
+                {
+                    $receiveHeaders = [];
+                    $curlResult = curl_exec($handler);
+                    $this->result = $this->getResponse($request, $handler, $curlResult, $receiveHeaders);
+                    $result = &$this->result;
+                    $statusCode = $result->getStatusCode();
+                    // 状态码为5XX或者0才需要重试
+                    if (!(0 === $statusCode || (5 === (int) ($statusCode / 100))))
+                    {
+                        break;
+                    }
+                }
+                if ($request->getAttribute(Attributes::FOLLOW_LOCATION, true) && ($statusCode >= 300 && $statusCode < 400) && $result && '' !== ($location = $result->getHeaderLine('location')))
+                {
+                    $maxRedirects = $request->getAttribute(Attributes::MAX_REDIRECTS, 10);
+                    if (++$redirectCount <= $maxRedirects)
+                    {
+                        // 重定向清除之前下载的文件
+                        if (null !== $saveFileFp)
+                        {
+                            ftruncate($saveFileFp, 0);
+                            fseek($saveFileFp, 0);
+                        }
+                        $isLocation = true;
+                        $uri = $this->parseRedirectLocation($location, $uri);
+                        continue;
+                    }
+                    else
+                    {
+                        $result = $result->withErrno(-1)
+                                        ->withError(sprintf('Maximum (%s) redirects followed', $maxRedirects));
+                    }
+                }
+                break;
+            } while (true);
+            // 关闭保存至文件的句柄
+            if (null !== $saveFileFp)
+            {
+                fclose($saveFileFp);
+                $saveFileFp = null;
+            }
+        }
+        finally
+        {
+            if ($poolIsEnabled && $this->handler)
+            {
+                // @phpstan-ignore-next-line
+                $httpConnectionManager->release($this->poolKey, $this->handler);
+                $this->handler = null;
+            }
+        }
+    }
+
+    /**
+     * 构建基础 Curl Handler.
+     *
+     * @param \Yurun\Util\YurunHttp\Http\Request $request
+     * @param resource                           $handler
+     * @param array|null                         $headers
+     * @param resource|null                      $saveFileFp
+     *
+     * @return void
+     */
+    public function buildCurlHandlerBase(&$request, $handler, &$headers = null, &$saveFileFp = null)
+    {
+        $options = [
+            // 返回内容
+            \CURLOPT_RETURNTRANSFER  => true,
+            // 保存cookie
+            \CURLOPT_COOKIEJAR       => 'php://memory',
+            // 允许复用连接
+            \CURLOPT_FORBID_REUSE    => false,
+        ];
+        // 自动重定向
+        $options[\CURLOPT_MAXREDIRS] = $request->getAttribute(Attributes::MAX_REDIRECTS, 10);
+
+        // 自动解压缩支持
+        $acceptEncoding = $request->getHeaderLine('Accept-Encoding');
+        if ('' !== $acceptEncoding)
+        {
+            $options[\CURLOPT_ENCODING] = $acceptEncoding;
+        }
+        else
+        {
+            $options[\CURLOPT_ENCODING] = '';
+        }
+        curl_setopt_array($handler, $options);
+        $this->parseSSL($request, $handler);
+        $this->parseOptions($request, $handler, $headers, $saveFileFp);
+        $this->parseProxy($request, $handler);
+        $this->parseHeaders($request, $handler);
+        $this->parseCookies($request, $handler);
+        $this->parseNetwork($request, $handler);
+    }
+
+    /**
+     * 构建扩展 Curl Handler.
+     *
+     * @param \Yurun\Util\YurunHttp\Http\Request $request
+     * @param resource                           $handler
+     * @param UriInterface|null                  $uri
+     * @param string|null                        $method
+     * @param string|null                        $body
+     *
+     * @return void
+     */
+    public function buildCurlHandlerEx(&$request, $handler, $uri = null, $method = null, $body = null)
+    {
+        if (null === $uri)
+        {
+            $uri = $request->getUri();
+        }
+        if (null === $method)
+        {
+            $method = $request->getMethod();
+        }
+        if (null === $body)
+        {
+            $body = (string) $request->getBody();
+        }
+        switch ($request->getProtocolVersion())
+        {
+            case '1.0':
+                $httpVersion = \CURL_HTTP_VERSION_1_0;
+                break;
+            case '2.0':
+                $ssl = 'https' === $uri->getScheme();
+                if ($ssl)
+                {
+                    $httpVersion = \CURL_HTTP_VERSION_2TLS;
+                }
+                else
+                {
+                    $httpVersion = \CURL_HTTP_VERSION_2;
+                }
+                break;
+            default:
+                $httpVersion = \CURL_HTTP_VERSION_1_1;
+        }
+        $requestOptions = [
+            \CURLOPT_URL             => (string) $uri,
+            \CURLOPT_HTTP_VERSION    => $httpVersion,
+        ];
+        // 请求方法
+        if ($body && 'GET' !== $method)
+        {
+            $requestOptions[\CURLOPT_POSTFIELDS] = $body;
+        }
+        $requestOptions[\CURLOPT_CUSTOMREQUEST] = $method;
+        if ('HEAD' === $method)
+        {
+            $requestOptions[\CURLOPT_NOBODY] = true;
+        }
+        curl_setopt_array($handler, $requestOptions);
+    }
+
+    /**
+     * 接收请求
+     *
+     * @return \Yurun\Util\YurunHttp\Http\Response|null
+     */
+    public function recv()
+    {
+        return $this->result;
+    }
+
+    /**
+     * 获取响应对象
+     *
+     * @param \Yurun\Util\YurunHttp\Http\Request $request
+     * @param resource                           $handler
+     * @param string|bool                        $body
+     * @param array                              $receiveHeaders
+     *
+     * @return \Yurun\Util\YurunHttp\Http\Response
+     */
+    private function getResponse($request, $handler, $body, $receiveHeaders)
+    {
+        // PHP 7.0.0开始substr()的 string 字符串长度与 start 相同时将返回一个空字符串。在之前的版本中,这种情况将返回 FALSE 。
+        if (false === $body)
+        {
+            $body = '';
+        }
+
+        // body
+        $result = new Response($body, curl_getinfo($handler, \CURLINFO_HTTP_CODE));
+
+        // headers
+        $rawHeaders = implode('', $receiveHeaders);
+        $headers = $this->parseHeaderOneRequest($rawHeaders);
+        foreach ($headers as $name => $value)
+        {
+            $result = $result->withAddedHeader($name, $value);
+        }
+
+        // cookies
+        $cookies = [];
+        $count = preg_match_all('/([^\r\n]+)/i', implode(\PHP_EOL, $result->getHeader('set-cookie')), $matches);
+        $cookieManager = $this->cookieManager;
+        for ($i = 0; $i < $count; ++$i)
+        {
+            $cookieItem = $cookieManager->addSetCookie($matches[1][$i]);
+            $cookies[$cookieItem->name] = (array) $cookieItem;
+        }
+
+        // 下载文件名
+        if ($savedFileName = $request->getAttribute(Attributes::SAVE_FILE_PATH))
+        {
+            $result = $result->withSavedFileName($savedFileName);
+        }
+
+        return $result->withRequest($request)
+                      ->withCookieOriginParams($cookies)
+                      ->withError(curl_error($handler))
+                      ->withErrno(curl_errno($handler));
+    }
+
+    /**
+     * parseHeaderOneRequest.
+     *
+     * @param string $piece
+     *
+     * @return array
+     */
+    private function parseHeaderOneRequest($piece)
+    {
+        $tmpHeaders = [];
+        $lines = explode("\r\n", $piece);
+        $linesCount = \count($lines);
+        //从1开始,第0行包含了协议信息和状态信息,排除该行
+        for ($i = 1; $i < $linesCount; ++$i)
+        {
+            $line = trim($lines[$i]);
+            if (empty($line) || false == strstr($line, ':'))
+            {
+                continue;
+            }
+            list($key, $value) = explode(':', $line, 2);
+            $key = trim($key);
+            $value = trim($value);
+            if (isset($tmpHeaders[$key]))
+            {
+                if (\is_array($tmpHeaders[$key]))
+                {
+                    $tmpHeaders[$key][] = $value;
+                }
+                else
+                {
+                    $tmp = $tmpHeaders[$key];
+                    $tmpHeaders[$key] = [
+                        $tmp,
+                        $value,
+                    ];
+                }
+            }
+            else
+            {
+                $tmpHeaders[$key] = $value;
+            }
+        }
+
+        return $tmpHeaders;
+    }
+
+    /**
+     * 处理加密访问.
+     *
+     * @param \Yurun\Util\YurunHttp\Http\Request $request
+     * @param resource                           $handler
+     *
+     * @return void
+     */
+    private function parseSSL(&$request, $handler)
+    {
+        if ($request->getAttribute(Attributes::IS_VERIFY_CA, false))
+        {
+            curl_setopt_array($handler, [
+                \CURLOPT_SSL_VERIFYPEER    => true,
+                \CURLOPT_CAINFO            => $request->getAttribute(Attributes::CA_CERT),
+                \CURLOPT_SSL_VERIFYHOST    => 2,
+            ]);
+        }
+        else
+        {
+            curl_setopt_array($handler, [
+                \CURLOPT_SSL_VERIFYPEER    => false,
+                \CURLOPT_SSL_VERIFYHOST    => 0,
+            ]);
+        }
+        $certPath = $request->getAttribute(Attributes::CERT_PATH, '');
+        if ('' !== $certPath)
+        {
+            curl_setopt_array($handler, [
+                \CURLOPT_SSLCERT         => $certPath,
+                \CURLOPT_SSLCERTPASSWD   => $request->getAttribute(Attributes::CERT_PASSWORD),
+                \CURLOPT_SSLCERTTYPE     => $request->getAttribute(Attributes::CERT_TYPE, 'pem'),
+            ]);
+        }
+        $keyPath = $request->getAttribute(Attributes::KEY_PATH, '');
+        if ('' !== $keyPath)
+        {
+            curl_setopt_array($handler, [
+                \CURLOPT_SSLKEY          => $keyPath,
+                \CURLOPT_SSLKEYPASSWD    => $request->getAttribute(Attributes::KEY_PASSWORD),
+                \CURLOPT_SSLKEYTYPE      => $request->getAttribute(Attributes::KEY_TYPE, 'pem'),
+            ]);
+        }
+    }
+
+    /**
+     * 处理设置项.
+     *
+     * @param \Yurun\Util\YurunHttp\Http\Request $request
+     * @param resource                           $handler
+     * @param array                              $headers
+     * @param resource|null                      $saveFileFp
+     *
+     * @return void
+     */
+    private function parseOptions(&$request, $handler, &$headers = null, &$saveFileFp = null)
+    {
+        $options = $request->getAttribute(Attributes::OPTIONS, []);
+        if (isset($options[\CURLOPT_HEADERFUNCTION]))
+        {
+            $headerCallable = $options[\CURLOPT_HEADERFUNCTION];
+        }
+        else
+        {
+            $headerCallable = null;
+        }
+        $headers = [];
+        $options[\CURLOPT_HEADERFUNCTION] = function ($handler, $header) use ($headerCallable, &$headers) {
+            $headers[] = $header;
+            if ($headerCallable)
+            {
+                $headerCallable($handler, $header);
+            }
+
+            return \strlen($header);
+        };
+        curl_setopt_array($handler, $options);
+        // 请求结果保存为文件
+        if (null !== ($saveFilePath = $request->getAttribute(Attributes::SAVE_FILE_PATH)))
+        {
+            $last = substr($saveFilePath, -1, 1);
+            if ('/' === $last || '\\' === $last)
+            {
+                // 自动获取文件名
+                $saveFilePath .= basename($request->getUri()->__toString());
+            }
+            $saveFileFp = fopen($saveFilePath, $request->getAttribute(Attributes::SAVE_FILE_MODE, 'w+'));
+            curl_setopt_array($handler, [
+                \CURLOPT_HEADER          => false,
+                \CURLOPT_RETURNTRANSFER  => false,
+                \CURLOPT_FILE            => $saveFileFp,
+            ]);
+        }
+    }
+
+    /**
+     * 处理代理.
+     *
+     * @param \Yurun\Util\YurunHttp\Http\Request $request
+     * @param resource                           $handler
+     *
+     * @return void
+     */
+    private function parseProxy(&$request, $handler)
+    {
+        if ($request->getAttribute(Attributes::USE_PROXY, false))
+        {
+            $type = $request->getAttribute(Attributes::PROXY_TYPE, 'http');
+            curl_setopt_array($handler, [
+                \CURLOPT_PROXYAUTH    => self::$proxyAuths[$request->getAttribute(Attributes::PROXY_AUTH, 'basic')],
+                \CURLOPT_PROXY        => $request->getAttribute(Attributes::PROXY_SERVER),
+                \CURLOPT_PROXYPORT    => $request->getAttribute(Attributes::PROXY_PORT),
+                \CURLOPT_PROXYUSERPWD => $request->getAttribute(Attributes::PROXY_USERNAME, '') . ':' . $request->getAttribute(Attributes::PROXY_PASSWORD, ''),
+                \CURLOPT_PROXYTYPE    => 'socks5' === $type ? (\defined('CURLPROXY_SOCKS5_HOSTNAME') ? \CURLPROXY_SOCKS5_HOSTNAME : self::$proxyType[$type]) : self::$proxyType[$type],
+            ]);
+        }
+    }
+
+    /**
+     * 处理headers.
+     *
+     * @param \Yurun\Util\YurunHttp\Http\Request $request
+     * @param resource                           $handler
+     *
+     * @return void
+     */
+    private function parseHeaders(&$request, $handler)
+    {
+        if (!$request->hasHeader('User-Agent'))
+        {
+            $request = $request->withHeader('User-Agent', $request->getAttribute(Attributes::USER_AGENT, static::$defaultUA));
+        }
+        if (!$request->hasHeader('Connection'))
+        {
+            $request = $request->withHeader('Connection', 'Keep-Alive')->withHeader('Keep-Alive', '300');
+        }
+        curl_setopt($handler, \CURLOPT_HTTPHEADER, $this->parseHeadersFormat($request));
+    }
+
+    /**
+     * 处理成CURL可以识别的headers格式.
+     *
+     * @param \Yurun\Util\YurunHttp\Http\Request $request
+     *
+     * @return array
+     */
+    private function parseHeadersFormat($request)
+    {
+        $headers = [];
+        foreach ($request->getHeaders() as $name => $value)
+        {
+            $headers[] = $name . ': ' . implode(',', $value);
+        }
+
+        return $headers;
+    }
+
+    /**
+     * 处理cookie.
+     *
+     * @param \Yurun\Util\YurunHttp\Http\Request $request
+     * @param resource                           $handler
+     *
+     * @return void
+     */
+    private function parseCookies(&$request, $handler)
+    {
+        $cookieManager = $this->cookieManager;
+        foreach ($request->getCookieParams() as $name => $value)
+        {
+            $cookieManager->setCookie($name, $value);
+        }
+        $cookie = $cookieManager->getRequestCookieString($request->getUri());
+        curl_setopt($handler, \CURLOPT_COOKIE, $cookie);
+    }
+
+    /**
+     * 处理网络相关.
+     *
+     * @param \Yurun\Util\YurunHttp\Http\Request $request
+     * @param resource                           $handler
+     *
+     * @return void
+     */
+    private function parseNetwork(&$request, $handler)
+    {
+        // 用户名密码处理
+        $username = $request->getAttribute(Attributes::USERNAME);
+        if (null != $username)
+        {
+            $userPwd = $username . ':' . $request->getAttribute(Attributes::PASSWORD, '');
+        }
+        else
+        {
+            $userPwd = '';
+        }
+        curl_setopt_array($handler, [
+            // 连接超时
+            \CURLOPT_CONNECTTIMEOUT_MS       => $request->getAttribute(Attributes::CONNECT_TIMEOUT, 30000),
+            // 总超时
+            \CURLOPT_TIMEOUT_MS              => $request->getAttribute(Attributes::TIMEOUT, 0),
+            // 下载限速
+            \CURLOPT_MAX_RECV_SPEED_LARGE    => $request->getAttribute(Attributes::DOWNLOAD_SPEED),
+            // 上传限速
+            \CURLOPT_MAX_SEND_SPEED_LARGE    => $request->getAttribute(Attributes::UPLOAD_SPEED),
+            // 连接中用到的用户名和密码
+            \CURLOPT_USERPWD                 => $userPwd,
+        ]);
+    }
+
+    /**
+     * 连接 WebSocket.
+     *
+     * @param \Yurun\Util\YurunHttp\Http\Request               $request
+     * @param \Yurun\Util\YurunHttp\WebSocket\IWebSocketClient $websocketClient
+     *
+     * @return \Yurun\Util\YurunHttp\WebSocket\IWebSocketClient
+     */
+    public function websocket(&$request, $websocketClient = null)
+    {
+        throw new \RuntimeException('Curl Handler does not support WebSocket');
+    }
+
+    /**
+     * 获取原始处理器对象
+     *
+     * @return mixed
+     */
+    public function getHandler()
+    {
+        return $this->handler;
+    }
+
+    /**
+     * 批量运行并发请求
+     *
+     * @param \Yurun\Util\YurunHttp\Http\Request[] $requests
+     * @param float|null                           $timeout  超时时间,单位:秒。默认为 null 不限制
+     *
+     * @return \Yurun\Util\YurunHttp\Http\Response[]
+     */
+    public function coBatch($requests, $timeout = null)
+    {
+        $this->checkRequests($requests);
+        $mh = curl_multi_init();
+        $curlHandlers = $recvHeaders = $saveFileFps = [];
+        $result = [];
+        $needRedirectRequests = [];
+        try
+        {
+            foreach ($requests as $k => $request)
+            {
+                $result[$k] = null;
+                $curlHandler = curl_init();
+                $recvHeaders[$k] = $saveFileFps[$k] = null;
+                $this->buildCurlHandlerBase($request, $curlHandler, $recvHeaders[$k], $saveFileFps[$k]);
+                $files = $request->getUploadedFiles();
+                $body = (string) $request->getBody();
+                if (!empty($files))
+                {
+                    $body = FormDataBuilder::build($body, $files, $boundary);
+                    $request = $request->withHeader('Content-Type', MediaType::MULTIPART_FORM_DATA . '; boundary=' . $boundary);
+                }
+                $this->buildCurlHandlerEx($request, $curlHandler, null, null, $body);
+                curl_multi_add_handle($mh, $curlHandler);
+                $curlHandlers[$k] = $curlHandler;
+            }
+            $running = null;
+            $beginTime = microtime(true);
+            // 执行批处理句柄
+            do
+            {
+                curl_multi_exec($mh, $running);
+                if ($running > 0)
+                {
+                    if ($timeout && microtime(true) - $beginTime >= $timeout)
+                    {
+                        break;
+                    }
+                    usleep(5000); // 每次延时 5 毫秒
+                }
+                else
+                {
+                    break;
+                }
+            } while (true);
+            foreach ($requests as $k => $request)
+            {
+                $handler = $curlHandlers[$k];
+                $receiveHeaders = $recvHeaders[$k];
+                $curlResult = curl_multi_getcontent($handler);
+                // @phpstan-ignore-next-line
+                $response = $this->getResponse($request, $handler, $curlResult, $receiveHeaders);
+                // 重定向处理
+                $statusCode = $response->getStatusCode();
+                $redirectCount = $request->getAttribute(Attributes::PRIVATE_REDIRECT_COUNT);
+                if ($request->getAttribute(Attributes::FOLLOW_LOCATION, true) && ($statusCode >= 300 && $statusCode < 400) && '' !== ($location = $response->getHeaderLine('location')))
+                {
+                    $maxRedirects = $request->getAttribute(Attributes::MAX_REDIRECTS, 10);
+                    if (++$redirectCount <= $maxRedirects)
+                    {
+                        $request = $request->withAttribute(Attributes::PRIVATE_REDIRECT_COUNT, $redirectCount);
+                        if (\in_array($statusCode, [301, 302, 303]))
+                        {
+                            $request = $request->withMethod('GET')->withBody(new MemoryStream());
+                        }
+                        $request = $request->withUri($this->parseRedirectLocation($location, $request->getUri()));
+                        $needRedirectRequests[$k] = $request;
+                        continue;
+                    }
+                    else
+                    {
+                        $response = $response->withErrno(-1)
+                                                    ->withError(sprintf('Maximum (%s) redirects followed', $maxRedirects));
+                    }
+                }
+                $result[$k] = $response;
+            }
+        }
+        finally
+        {
+            foreach ($saveFileFps as $fp)
+            {
+                // @phpstan-ignore-next-line
+                if ($fp)
+                {
+                    fclose($fp);
+                }
+            }
+            foreach ($curlHandlers as $curlHandler)
+            {
+                curl_multi_remove_handle($mh, $curlHandler);
+                curl_close($curlHandler);
+            }
+            curl_multi_close($mh);
+        }
+        if ($needRedirectRequests)
+        {
+            foreach ($this->coBatch($needRedirectRequests, $timeout) as $k => $response)
+            {
+                $result[$k] = $response;
+            }
+        }
+
+        return $result;
+    }
+}

+ 127 - 0
vendor/yurunsoft/yurun-http/src/YurunHttp/Handler/Curl/CurlConnectionPool.php

@@ -0,0 +1,127 @@
+<?php
+
+namespace Yurun\Util\YurunHttp\Handler\Curl;
+
+use Yurun\Util\YurunHttp\Pool\BaseConnectionPool;
+
+class CurlConnectionPool extends BaseConnectionPool
+{
+    /**
+     * 队列.
+     *
+     * @var \SplQueue
+     */
+    protected $queue;
+
+    /**
+     * 连接数组.
+     *
+     * @var array
+     */
+    protected $connections = [];
+
+    /**
+     * @param \Yurun\Util\YurunHttp\Pool\Config\PoolConfig $config
+     */
+    public function __construct($config)
+    {
+        parent::__construct($config);
+        $this->queue = new \SplQueue();
+    }
+
+    /**
+     * 关闭连接池和连接池中的连接.
+     *
+     * @return void
+     */
+    public function close()
+    {
+        $connections = $this->connections;
+        $this->connections = [];
+        $this->queue = new \SplQueue();
+        foreach ($connections as $connection)
+        {
+            curl_close($connection);
+        }
+    }
+
+    /**
+     * 创建一个连接,但不受连接池管理.
+     *
+     * @return mixed
+     */
+    public function createConnection()
+    {
+        return curl_init();
+    }
+
+    /**
+     * 获取连接.
+     *
+     * @return mixed
+     */
+    public function getConnection()
+    {
+        if ($this->getFree() > 0)
+        {
+            return $this->queue->dequeue();
+        }
+        else
+        {
+            $maxConnections = $this->getConfig()->getMaxConnections();
+            if (0 != $maxConnections && $this->getCount() >= $maxConnections)
+            {
+                return false;
+            }
+            else
+            {
+                return $this->connections[] = $this->createConnection();
+            }
+        }
+    }
+
+    /**
+     * 释放连接占用.
+     *
+     * @param mixed $connection
+     *
+     * @return void
+     */
+    public function release($connection)
+    {
+        if (\in_array($connection, $this->connections))
+        {
+            $this->queue->enqueue($connection);
+        }
+    }
+
+    /**
+     * 获取当前池子中连接总数.
+     *
+     * @return int
+     */
+    public function getCount()
+    {
+        return \count($this->connections);
+    }
+
+    /**
+     * 获取当前池子中空闲连接总数.
+     *
+     * @return int
+     */
+    public function getFree()
+    {
+        return $this->queue->count();
+    }
+
+    /**
+     * 获取当前池子中正在使用的连接总数.
+     *
+     * @return int
+     */
+    public function getUsed()
+    {
+        return $this->getCount() - $this->getFree();
+    }
+}

+ 143 - 0
vendor/yurunsoft/yurun-http/src/YurunHttp/Handler/Curl/CurlHttpConnectionManager.php

@@ -0,0 +1,143 @@
+<?php
+
+namespace Yurun\Util\YurunHttp\Handler\Curl;
+
+use Yurun\Util\YurunHttp\ConnectionPool;
+use Yurun\Util\YurunHttp\Handler\Contract\IConnectionManager;
+use Yurun\Util\YurunHttp\Pool\Contract\IConnectionPool;
+use Yurun\Util\YurunHttp\Pool\Traits\TConnectionPoolConfigs;
+
+class CurlHttpConnectionManager implements IConnectionManager
+{
+    use TConnectionPoolConfigs;
+
+    /**
+     * 连接池集合.
+     *
+     * @var CurlConnectionPool[]
+     */
+    private $connectionPools = [];
+
+    /**
+     * @var static
+     */
+    private static $instance;
+
+    /**
+     * @return static
+     */
+    public static function getInstance()
+    {
+        if (null === self::$instance)
+        {
+            return self::$instance = new static();
+        }
+
+        return self::$instance;
+    }
+
+    /**
+     * 获取连接池数组.
+     *
+     * @return IConnectionPool[]
+     */
+    public function getConnectionPools()
+    {
+        return $this->connectionPools;
+    }
+
+    /**
+     * 获取连接池对象
+     *
+     * @param string $url
+     *
+     * @return IConnectionPool
+     */
+    public function getConnectionPool($url)
+    {
+        if (isset($this->connectionPools[$url]))
+        {
+            return $this->connectionPools[$url];
+        }
+        else
+        {
+            $config = ConnectionPool::getConfig($url);
+            if (null === $config)
+            {
+                ConnectionPool::setConfig($url);
+            }
+            $config = ConnectionPool::getConfig($url);
+
+            return $this->connectionPools[$url] = new CurlConnectionPool($config);
+        }
+    }
+
+    /**
+     * 获取连接.
+     *
+     * @param string $url
+     *
+     * @return mixed
+     */
+    public function getConnection($url)
+    {
+        $connectionPool = $this->getConnectionPool($url);
+
+        return $connectionPool->getConnection();
+    }
+
+    /**
+     * 释放连接占用.
+     *
+     * @param string $url
+     * @param mixed  $connection
+     *
+     * @return void
+     */
+    public function release($url, $connection)
+    {
+        $connectionPool = $this->getConnectionPool($url);
+        $connectionPool->release($connection);
+    }
+
+    /**
+     * 关闭指定连接.
+     *
+     * @param string $url
+     *
+     * @return bool
+     */
+    public function closeConnection($url)
+    {
+        return false;
+    }
+
+    /**
+     * 创建新连接,但不归本管理器管理.
+     *
+     * @param string $url
+     *
+     * @return mixed
+     */
+    public function createConnection($url)
+    {
+        $connectionPool = $this->getConnectionPool($url);
+
+        return $connectionPool->createConnection();
+    }
+
+    /**
+     * 关闭连接管理器.
+     *
+     * @return void
+     */
+    public function close()
+    {
+        $connectionPools = $this->connectionPools;
+        $this->connectionPools = [];
+        foreach ($connectionPools as $connectionPool)
+        {
+            $connectionPool->close();
+        }
+    }
+}

+ 63 - 0
vendor/yurunsoft/yurun-http/src/YurunHttp/Handler/IHandler.php

@@ -0,0 +1,63 @@
+<?php
+
+namespace Yurun\Util\YurunHttp\Handler;
+
+interface IHandler
+{
+    /**
+     * 发送请求
+     *
+     * @param \Yurun\Util\YurunHttp\Http\Request $request
+     *
+     * @return void
+     */
+    public function send(&$request);
+
+    /**
+     * 接收请求
+     *
+     * @return \Yurun\Util\YurunHttp\Http\Response|null
+     */
+    public function recv();
+
+    /**
+     * 连接 WebSocket.
+     *
+     * @param \Yurun\Util\YurunHttp\Http\Request               $request
+     * @param \Yurun\Util\YurunHttp\WebSocket\IWebSocketClient $websocketClient
+     *
+     * @return \Yurun\Util\YurunHttp\WebSocket\IWebSocketClient
+     */
+    public function websocket(&$request, $websocketClient = null);
+
+    /**
+     * Get cookie 管理器.
+     *
+     * @return \Yurun\Util\YurunHttp\Cookie\CookieManager
+     */
+    public function getCookieManager();
+
+    /**
+     * 获取原始处理器对象
+     *
+     * @return mixed
+     */
+    public function getHandler();
+
+    /**
+     * 批量运行并发请求
+     *
+     * @param \Yurun\Util\YurunHttp\Http\Request[] $requests
+     * @param float|null                           $timeout  超时时间,单位:秒。默认为 null 不限制
+     *
+     * @return \Yurun\Util\YurunHttp\Http\Response[]
+     */
+    public function coBatch($requests, $timeout = null);
+
+    /**
+     * 关闭并释放所有资源.
+     *
+     * @return void
+     */
+    public function close();
+}

+ 818 - 0
vendor/yurunsoft/yurun-http/src/YurunHttp/Handler/Swoole.php

@@ -0,0 +1,818 @@
+<?php
+
+namespace Yurun\Util\YurunHttp\Handler;
+
+use Swoole\Http2\Request as Http2Request;
+use Yurun\Util\YurunHttp;
+use Yurun\Util\YurunHttp\Attributes;
+use Yurun\Util\YurunHttp\ConnectionPool;
+use Yurun\Util\YurunHttp\Exception\WebSocketException;
+use Yurun\Util\YurunHttp\Handler\Swoole\SwooleHttp2ConnectionManager;
+use Yurun\Util\YurunHttp\Handler\Swoole\SwooleHttpConnectionManager;
+use Yurun\Util\YurunHttp\Http\Psr7\Consts\MediaType;
+use Yurun\Util\YurunHttp\Http\Psr7\Uri;
+use Yurun\Util\YurunHttp\Http\Response;
+use Yurun\Util\YurunHttp\Traits\TCookieManager;
+use Yurun\Util\YurunHttp\Traits\THandler;
+
+class Swoole implements IHandler
+{
+    use TCookieManager;
+    use THandler;
+
+    /**
+     * http 连接管理器.
+     *
+     * @var SwooleHttpConnectionManager
+     */
+    private $httpConnectionManager;
+
+    /**
+     * http2 连接管理器.
+     *
+     * @var SwooleHttp2ConnectionManager
+     */
+    private $http2ConnectionManager;
+
+    /**
+     * 请求结果.
+     *
+     * @var \Yurun\Util\YurunHttp\Http\Response
+     */
+    private $result;
+
+    /**
+     * 连接池键.
+     *
+     * @var string
+     */
+    private $poolKey;
+
+    /**
+     * 连接池是否启用.
+     *
+     * @var bool
+     */
+    private $poolIsEnabled = false;
+
+    /**
+     * 本 Handler 默认的 User-Agent.
+     *
+     * @var string
+     */
+    private static $defaultUA;
+
+    public function __construct()
+    {
+        if (null === static::$defaultUA)
+        {
+            static::$defaultUA = sprintf('Mozilla/5.0 YurunHttp/%s Swoole/%s', YurunHttp::VERSION, \defined('SWOOLE_VERSION') ? \SWOOLE_VERSION : 'unknown');
+        }
+        $this->initCookieManager();
+        $this->httpConnectionManager = new SwooleHttpConnectionManager();
+        $this->http2ConnectionManager = new SwooleHttp2ConnectionManager();
+    }
+
+    /**
+     * 关闭并释放所有资源.
+     *
+     * @return void
+     */
+    public function close()
+    {
+        $this->httpConnectionManager->close();
+        $this->http2ConnectionManager->close();
+    }
+
+    /**
+     * 构建请求
+     *
+     * @param \Yurun\Util\YurunHttp\Http\Request                           $request
+     * @param \Swoole\Coroutine\Http\Client|\Swoole\Coroutine\Http2\Client $connection
+     * @param Http2Request                                                 $http2Request
+     *
+     * @return void
+     */
+    public function buildRequest($request, $connection, &$http2Request)
+    {
+        if ($isHttp2 = '2.0' === $request->getProtocolVersion())
+        {
+            $http2Request = new Http2Request();
+        }
+        else
+        {
+            $http2Request = null;
+        }
+        $uri = $request->getUri();
+        // method
+        if ($isHttp2)
+        {
+            $http2Request->method = $request->getMethod();
+        }
+        else
+        {
+            $connection->setMethod($request->getMethod());
+        }
+        // cookie
+        $this->parseCookies($request, $connection, $http2Request);
+        // body
+        $hasFile = false;
+        $redirectCount = $request->getAttribute(Attributes::PRIVATE_REDIRECT_COUNT, 0);
+        if ($redirectCount <= 0)
+        {
+            $files = $request->getUploadedFiles();
+            $body = (string) $request->getBody();
+            if (!empty($files))
+            {
+                if ($isHttp2)
+                {
+                    throw new \RuntimeException('Http2 swoole handler does not support upload file');
+                }
+                $hasFile = true;
+                foreach ($files as $name => $file)
+                {
+                    $connection->addFile($file->getTempFileName(), $name, $file->getClientMediaType(), basename($file->getClientFilename()));
+                }
+                parse_str($body, $body);
+            }
+            if ($isHttp2)
+            {
+                $http2Request->data = $body;
+            }
+            else
+            {
+                $connection->setData($body);
+            }
+        }
+        // 其它处理
+        $this->parseSSL($request);
+        $this->parseProxy($request);
+        $this->parseNetwork($request);
+        // 设置客户端参数
+        $settings = $request->getAttribute(Attributes::OPTIONS, []);
+        if ($settings)
+        {
+            $connection->set($settings);
+        }
+        // headers
+        if (!$request->hasHeader('Host'))
+        {
+            $request = $request->withHeader('Host', Uri::getDomain($uri));
+        }
+        if (!$hasFile && !$request->hasHeader('Content-Type'))
+        {
+            $request = $request->withHeader('Content-Type', MediaType::APPLICATION_FORM_URLENCODED);
+        }
+        if (!$request->hasHeader('User-Agent'))
+        {
+            $request = $request->withHeader('User-Agent', $request->getAttribute(Attributes::USER_AGENT, static::$defaultUA));
+        }
+        $headers = [];
+        foreach ($request->getHeaders() as $name => $value)
+        {
+            $headers[$name] = implode(',', $value);
+        }
+        if ($isHttp2)
+        {
+            $http2Request->headers = $headers;
+            $http2Request->pipeline = $request->getAttribute(Attributes::HTTP2_PIPELINE, false);
+            $path = $uri->getPath();
+            if ('' === $path)
+            {
+                $path = '/';
+            }
+            $query = $uri->getQuery();
+            if ('' !== $query)
+            {
+                $path .= '?' . $query;
+            }
+            $http2Request->path = $path;
+        }
+        else
+        {
+            $connection->setHeaders($headers);
+        }
+    }
+
+    /**
+     * 发送请求
+     *
+     * @param \Yurun\Util\YurunHttp\Http\Request $request
+     *
+     * @return bool
+     */
+    public function send(&$request)
+    {
+        $this->poolIsEnabled = ConnectionPool::isEnabled() && false !== $request->getAttribute(Attributes::CONNECTION_POOL);
+        $request = $this->sendDefer($request);
+        if ($request->getAttribute(Attributes::PRIVATE_IS_HTTP2) && $request->getAttribute(Attributes::HTTP2_NOT_RECV))
+        {
+            return true;
+        }
+
+        return (bool) $this->recvDefer($request);
+    }
+
+    /**
+     * 发送请求,但延迟接收.
+     *
+     * @param \Yurun\Util\YurunHttp\Http\Request $request
+     *
+     * @return \Yurun\Util\YurunHttp\Http\Request
+     */
+    public function sendDefer($request)
+    {
+        $isHttp2 = '2.0' === $request->getProtocolVersion();
+        if ($poolIsEnabled = $this->poolIsEnabled)
+        {
+            if ($isHttp2)
+            {
+                $http2ConnectionManager = SwooleHttp2ConnectionManager::getInstance();
+            }
+            else
+            {
+                $httpConnectionManager = SwooleHttpConnectionManager::getInstance();
+            }
+        }
+        else
+        {
+            if ($isHttp2)
+            {
+                $http2ConnectionManager = $this->http2ConnectionManager;
+            }
+            else
+            {
+                $httpConnectionManager = $this->httpConnectionManager;
+            }
+        }
+        if ([] !== ($queryParams = $request->getQueryParams()))
+        {
+            $request = $request->withUri($request->getUri()->withQuery(http_build_query($queryParams, '', '&')));
+        }
+        $uri = $request->getUri();
+        try
+        {
+            $this->poolKey = $poolKey = ConnectionPool::getKey($uri);
+            if ($isHttp2)
+            {
+                /** @var \Swoole\Coroutine\Http2\Client $connection */
+                $connection = $http2ConnectionManager->getConnection($poolKey);
+            }
+            else
+            {
+                /** @var \Swoole\Coroutine\Http\Client $connection */
+                $connection = $httpConnectionManager->getConnection($poolKey);
+                $connection->setDefer(true);
+            }
+            $request = $request->withAttribute(Attributes::PRIVATE_POOL_KEY, $poolKey);
+            $isWebSocket = $request->getAttribute(Attributes::PRIVATE_WEBSOCKET, false);
+            // 构建
+            $this->buildRequest($request, $connection, $http2Request);
+            // 发送
+            $path = $uri->getPath();
+            if ('' === $path)
+            {
+                $path = '/';
+            }
+            $query = $uri->getQuery();
+            if ('' !== $query)
+            {
+                $path .= '?' . $query;
+            }
+            if ($isWebSocket)
+            {
+                if ($isHttp2)
+                {
+                    throw new \RuntimeException('Http2 swoole handler does not support websocket');
+                }
+                if (!$connection->upgrade($path))
+                {
+                    throw new WebSocketException(sprintf('WebSocket connect faled, statusCode: %s, error: %s, errorCode: %s', $connection->statusCode, swoole_strerror($connection->errCode), $connection->errCode), $connection->errCode);
+                }
+            }
+            elseif (null === ($saveFilePath = $request->getAttribute(Attributes::SAVE_FILE_PATH)))
+            {
+                if ($isHttp2)
+                {
+                    $result = $connection->send($http2Request);
+                    $request = $request->withAttribute(Attributes::PRIVATE_HTTP2_STREAM_ID, $result);
+                }
+                else
+                {
+                    $connection->execute($path);
+                }
+            }
+            else
+            {
+                if ($isHttp2)
+                {
+                    throw new \RuntimeException('Http2 swoole handler does not support download file');
+                }
+                $connection->download($path, $saveFilePath);
+            }
+
+            return $request->withAttribute(Attributes::PRIVATE_IS_HTTP2, $isHttp2)
+                        ->withAttribute(Attributes::PRIVATE_IS_WEBSOCKET, $isHttp2)
+                        ->withAttribute(Attributes::PRIVATE_CONNECTION, $connection);
+        }
+        catch (\Throwable $th)
+        {
+            throw $th;
+        }
+        finally
+        {
+            if ($poolIsEnabled && isset($connection) && isset($th))
+            {
+                if ($isHttp2)
+                {
+                    // @phpstan-ignore-next-line
+                    $http2ConnectionManager->release($poolKey, $connection);
+                }
+                else
+                {
+                    // @phpstan-ignore-next-line
+                    $httpConnectionManager->release($poolKey, $connection);
+                }
+            }
+        }
+    }
+
+    /**
+     * 延迟接收.
+     *
+     * @param \Yurun\Util\YurunHttp\Http\Request $request
+     * @param float|null                         $timeout
+     *
+     * @return \Yurun\Util\YurunHttp\Http\Response|bool
+     */
+    public function recvDefer($request, $timeout = null)
+    {
+        /** @var \Swoole\Coroutine\Http\Client|\Swoole\Coroutine\Http2\Client $connection */
+        $connection = $request->getAttribute(Attributes::PRIVATE_CONNECTION);
+        $poolKey = $request->getAttribute(Attributes::PRIVATE_POOL_KEY);
+        $retryCount = $request->getAttribute(Attributes::PRIVATE_RETRY_COUNT, 0);
+        $redirectCount = $request->getAttribute(Attributes::PRIVATE_REDIRECT_COUNT, 0);
+        $isHttp2 = '2.0' === $request->getProtocolVersion();
+        $isWebSocket = $request->getAttribute(Attributes::PRIVATE_WEBSOCKET, false);
+        try
+        {
+            $this->getResponse($request, $connection, $isWebSocket, $isHttp2, $timeout);
+        }
+        finally
+        {
+            if (!$isWebSocket)
+            {
+                if ($isHttp2)
+                {
+                    if ($this->poolIsEnabled)
+                    {
+                        $http2ConnectionManager = SwooleHttp2ConnectionManager::getInstance();
+                    }
+                    else
+                    {
+                        $http2ConnectionManager = $this->http2ConnectionManager;
+                    }
+                    $http2ConnectionManager->release($poolKey, $connection);
+                }
+                else
+                {
+                    if ($this->poolIsEnabled)
+                    {
+                        $httpConnectionManager = SwooleHttpConnectionManager::getInstance();
+                    }
+                    else
+                    {
+                        $httpConnectionManager = $this->httpConnectionManager;
+                    }
+                    $httpConnectionManager->release($poolKey, $connection);
+                }
+            }
+        }
+        $result = &$this->result;
+        $statusCode = $result->getStatusCode();
+        // 状态码为5XX或者0才需要重试
+        if ((0 === $statusCode || (5 === (int) ($statusCode / 100))) && $retryCount < $request->getAttribute(Attributes::RETRY, 0))
+        {
+            $request = $request->withAttribute(Attributes::RETRY, ++$retryCount);
+            $deferRequest = $this->sendDefer($request);
+
+            return $this->recvDefer($deferRequest, $timeout);
+        }
+        if (!$isWebSocket && $statusCode >= 300 && $statusCode < 400 && $request->getAttribute(Attributes::FOLLOW_LOCATION, true) && '' !== ($location = $result->getHeaderLine('location')))
+        {
+            if (++$redirectCount <= ($maxRedirects = $request->getAttribute(Attributes::MAX_REDIRECTS, 10)))
+            {
+                // 自己实现重定向
+                $uri = $this->parseRedirectLocation($location, $request->getUri());
+                if (\in_array($statusCode, [301, 302, 303]))
+                {
+                    $method = 'GET';
+                }
+                else
+                {
+                    $method = $request->getMethod();
+                }
+                $request = $request->withMethod($method)
+                                   ->withUri($uri)
+                                   ->withAttribute(Attributes::PRIVATE_REDIRECT_COUNT, $redirectCount);
+                $deferRequest = $this->sendDefer($request);
+
+                return $this->recvDefer($deferRequest, $timeout);
+            }
+            else
+            {
+                $result = $result->withErrno(-1)
+                                 ->withError(sprintf('Maximum (%s) redirects followed', $maxRedirects));
+
+                return false;
+            }
+        }
+        // 下载文件名
+        $savedFileName = $request->getAttribute(Attributes::SAVE_FILE_PATH);
+        if (null !== $savedFileName)
+        {
+            $result = $result->withSavedFileName($savedFileName);
+        }
+
+        return $result;
+    }
+
+    /**
+     * 连接 WebSocket.
+     *
+     * @param \Yurun\Util\YurunHttp\Http\Request               $request
+     * @param \Yurun\Util\YurunHttp\WebSocket\IWebSocketClient $websocketClient
+     *
+     * @return \Yurun\Util\YurunHttp\WebSocket\IWebSocketClient
+     */
+    public function websocket(&$request, $websocketClient = null)
+    {
+        if (!$websocketClient)
+        {
+            $websocketClient = new \Yurun\Util\YurunHttp\WebSocket\Swoole();
+        }
+        $request = $request->withAttribute(Attributes::PRIVATE_WEBSOCKET, true);
+        $this->send($request);
+        $websocketClient->init($this, $request, $this->result);
+
+        return $websocketClient;
+    }
+
+    /**
+     * 接收请求
+     *
+     * @return \Yurun\Util\YurunHttp\Http\Response|null
+     */
+    public function recv()
+    {
+        return $this->result;
+    }
+
+    /**
+     * 处理cookie.
+     *
+     * @param \Yurun\Util\YurunHttp\Http\Request $request
+     * @param mixed                              $connection
+     * @param Http2Request                       $http2Request
+     *
+     * @return void
+     */
+    private function parseCookies(&$request, $connection, $http2Request)
+    {
+        $cookieParams = $request->getCookieParams();
+        $cookieManager = $this->cookieManager;
+        foreach ($cookieParams as $name => $value)
+        {
+            $cookieManager->setCookie($name, $value);
+        }
+        $cookies = $cookieManager->getRequestCookies($request->getUri());
+        if ($http2Request)
+        {
+            $http2Request->cookies = $cookies;
+        }
+        else
+        {
+            $connection->setCookies($cookies);
+        }
+    }
+
+    /**
+     * 构建 Http2 Response.
+     *
+     * @param \Yurun\Util\YurunHttp\Http\Request $request
+     * @param \Swoole\Coroutine\Http2\Client     $connection
+     * @param \Swoole\Http2\Response|bool        $response
+     *
+     * @return \Yurun\Util\YurunHttp\Http\Response
+     */
+    public function buildHttp2Response($request, $connection, $response)
+    {
+        $success = false !== $response;
+        $result = new Response($response->data ?? '', $success ? $response->statusCode : 0);
+        if ($success)
+        {
+            // streamId
+            $result = $result->withStreamId($response->streamId);
+
+            // headers
+            if ($response->headers)
+            {
+                foreach ($response->headers as $name => $value)
+                {
+                    $result = $result->withHeader($name, $value);
+                }
+            }
+
+            // cookies
+            $cookies = [];
+            if (isset($response->set_cookie_headers))
+            {
+                $cookieManager = $this->cookieManager;
+                foreach ($response->set_cookie_headers as $value)
+                {
+                    $cookieItem = $cookieManager->addSetCookie($value);
+                    $cookies[$cookieItem->name] = (array) $cookieItem;
+                }
+            }
+            $result = $result->withCookieOriginParams($cookies);
+        }
+        if ($connection)
+        {
+            $result = $result->withError(swoole_strerror($connection->errCode))
+                             ->withErrno($connection->errCode);
+        }
+
+        return $result->withRequest($request);
+    }
+
+    /**
+     * 获取响应对象
+     *
+     * @param \Yurun\Util\YurunHttp\Http\Request                           $request
+     * @param \Swoole\Coroutine\Http\Client|\Swoole\Coroutine\Http2\Client $connection
+     * @param bool                                                         $isWebSocket
+     * @param bool                                                         $isHttp2
+     * @param float|null                                                   $timeout
+     *
+     * @return \Yurun\Util\YurunHttp\Http\Response
+     */
+    private function getResponse($request, $connection, $isWebSocket, $isHttp2, $timeout = null)
+    {
+        $result = &$this->result;
+        if ($isHttp2)
+        {
+            $response = $connection->recv($timeout);
+            $result = $this->buildHttp2Response($request, $connection, $response);
+        }
+        else
+        {
+            $success = $isWebSocket ? true : $connection->recv($timeout);
+            $result = new Response((string) $connection->body, $connection->statusCode);
+            if ($success)
+            {
+                // headers
+                if ($connection->headers)
+                {
+                    foreach ($connection->headers as $name => $value)
+                    {
+                        $result = $result->withHeader($name, $value);
+                    }
+                }
+
+                // cookies
+                $cookies = [];
+                if (isset($connection->set_cookie_headers))
+                {
+                    foreach ($connection->set_cookie_headers as $value)
+                    {
+                        $cookieItem = $this->cookieManager->addSetCookie($value);
+                        $cookies[$cookieItem->name] = (array) $cookieItem;
+                    }
+                }
+                $result = $result->withCookieOriginParams($cookies);
+            }
+            $result = $result->withRequest($request)
+                             ->withError(swoole_strerror($connection->errCode))
+                             ->withErrno($connection->errCode);
+        }
+
+        return $result;
+    }
+
+    /**
+     * 处理加密访问.
+     *
+     * @param \Yurun\Util\YurunHttp\Http\Request $request
+     *
+     * @return void
+     */
+    private function parseSSL(&$request)
+    {
+        $settings = $request->getAttribute(Attributes::OPTIONS, []);
+        if ($request->getAttribute(Attributes::IS_VERIFY_CA, false))
+        {
+            $settings['ssl_verify_peer'] = true;
+            $caCert = $request->getAttribute(Attributes::CA_CERT);
+            if (null !== $caCert)
+            {
+                $settings['ssl_cafile'] = $caCert;
+            }
+        }
+        else
+        {
+            $settings['ssl_verify_peer'] = false;
+        }
+        $certPath = $request->getAttribute(Attributes::CERT_PATH, '');
+        if ('' !== $certPath)
+        {
+            $settings['ssl_cert_file'] = $certPath;
+        }
+        $password = $request->getAttribute(Attributes::CERT_PASSWORD, '');
+        if ('' === $password)
+        {
+            $password = $request->getAttribute(Attributes::KEY_PASSWORD, '');
+        }
+        if ('' !== $password)
+        {
+            $settings['ssl_passphrase'] = $password;
+        }
+        $keyPath = $request->getAttribute(Attributes::KEY_PATH, '');
+        if ('' !== $keyPath)
+        {
+            $settings['ssl_key_file'] = $keyPath;
+        }
+        $request = $request->withAttribute(Attributes::OPTIONS, $settings);
+    }
+
+    /**
+     * 处理代理.
+     *
+     * @param \Yurun\Util\YurunHttp\Http\Request $request
+     *
+     * @return void
+     */
+    private function parseProxy(&$request)
+    {
+        $settings = $request->getAttribute(Attributes::OPTIONS, []);
+        if ($request->getAttribute(Attributes::USE_PROXY, false))
+        {
+            $type = $request->getAttribute(Attributes::PROXY_TYPE);
+            switch ($type)
+            {
+                case 'http':
+                    $settings['http_proxy_host'] = $request->getAttribute(Attributes::PROXY_SERVER);
+                    $port = $request->getAttribute(Attributes::PROXY_PORT);
+                    if (null !== $port)
+                    {
+                        $settings['http_proxy_port'] = $port;
+                    }
+                    $settings['http_proxy_user'] = $request->getAttribute(Attributes::PROXY_USERNAME);
+                    $password = $request->getAttribute(Attributes::PROXY_PASSWORD);
+                    if (null !== $password)
+                    {
+                        $settings['http_proxy_password'] = $password;
+                    }
+                    break;
+                case 'socks5':
+                    $settings['socks5_host'] = $request->getAttribute(Attributes::PROXY_SERVER);
+                    $port = $request->getAttribute(Attributes::PROXY_PORT);
+                    if (null !== $port)
+                    {
+                        $settings['socks5_port'] = $port;
+                    }
+                    $settings['socks5_username'] = $request->getAttribute(Attributes::PROXY_USERNAME);
+                    $password = $request->getAttribute(Attributes::PROXY_PASSWORD);
+                    if (null !== $password)
+                    {
+                        $settings['socks5_password'] = $password;
+                    }
+                    break;
+            }
+        }
+        $request = $request->withAttribute(Attributes::OPTIONS, $settings);
+    }
+
+    /**
+     * 处理网络相关.
+     *
+     * @param \Yurun\Util\YurunHttp\Http\Request $request
+     *
+     * @return void
+     */
+    private function parseNetwork(&$request)
+    {
+        $settings = $request->getAttribute(Attributes::OPTIONS, []);
+        // 用户名密码认证处理
+        $username = $request->getAttribute(Attributes::USERNAME);
+        if (null === $username)
+        {
+            $uri = $request->getUri();
+            $userInfo = $uri->getUserInfo();
+            if ($userInfo)
+            {
+                $authorization = 'Basic ' . base64_encode($userInfo);
+            }
+            else
+            {
+                $authorization = null;
+            }
+        }
+        else
+        {
+            $authorization = 'Basic ' . base64_encode($username . ':' . $request->getAttribute(Attributes::PASSWORD, ''));
+        }
+        if ($authorization)
+        {
+            $request = $request->withHeader('Authorization', $authorization);
+        }
+        // 超时
+        $settings['timeout'] = $request->getAttribute(Attributes::TIMEOUT, 30000) / 1000;
+        if ($settings['timeout'] < 0)
+        {
+            $settings['timeout'] = -1;
+        }
+        // 长连接
+        $settings['keep_alive'] = $request->getAttribute(Attributes::KEEP_ALIVE, true);
+        $request = $request->withAttribute(Attributes::OPTIONS, $settings);
+    }
+
+    /**
+     * 获取原始处理器对象
+     *
+     * @return mixed
+     */
+    public function getHandler()
+    {
+        return null;
+    }
+
+    /**
+     * Get http 连接管理器.
+     *
+     * @return \Yurun\Util\YurunHttp\Handler\Swoole\SwooleHttpConnectionManager
+     */
+    public function getSwooleHttpConnectionManager()
+    {
+        return $this->httpConnectionManager;
+    }
+
+    /**
+     * Get http2 连接管理器.
+     *
+     * @return SwooleHttp2ConnectionManager
+     */
+    public function getHttp2ConnectionManager()
+    {
+        return $this->http2ConnectionManager;
+    }
+
+    /**
+     * 批量运行并发请求
+     *
+     * @param \Yurun\Util\YurunHttp\Http\Request[] $requests
+     * @param float|null                           $timeout  超时时间,单位:秒。默认为 null 不限制
+     *
+     * @return \Yurun\Util\YurunHttp\Http\Response[]
+     */
+    public function coBatch($requests, $timeout = null)
+    {
+        /** @var Swoole[] $handlers */
+        $handlers = [];
+        $results = [];
+        foreach ($requests as $i => &$request)
+        {
+            $results[$i] = null;
+            $handlers[$i] = $handler = new self();
+            $request = $handler->sendDefer($request);
+        }
+        unset($request);
+        $beginTime = microtime(true);
+        $recvTimeout = null;
+        foreach ($requests as $i => $request)
+        {
+            if (null !== $timeout)
+            {
+                $recvTimeout = $timeout - (microtime(true) - $beginTime);
+                if ($recvTimeout <= 0)
+                {
+                    break;
+                }
+            }
+            $results[$i] = $handlers[$i]->recvDefer($request, $recvTimeout);
+        }
+
+        return $results;
+    }
+
+    public function getHttpConnectionManager(): SwooleHttpConnectionManager
+    {
+        if (ConnectionPool::isEnabled())
+        {
+            return SwooleHttpConnectionManager::getInstance();
+        }
+        else
+        {
+            return $this->httpConnectionManager;
+        }
+    }
+}

+ 143 - 0
vendor/yurunsoft/yurun-http/src/YurunHttp/Handler/Swoole/SwooleHttp2ConnectionManager.php

@@ -0,0 +1,143 @@
+<?php
+
+namespace Yurun\Util\YurunHttp\Handler\Swoole;
+
+use Yurun\Util\YurunHttp\ConnectionPool;
+use Yurun\Util\YurunHttp\Handler\Contract\IConnectionManager;
+use Yurun\Util\YurunHttp\Pool\Contract\IConnectionPool;
+use Yurun\Util\YurunHttp\Pool\Traits\TConnectionPoolConfigs;
+
+class SwooleHttp2ConnectionManager implements IConnectionManager
+{
+    use TConnectionPoolConfigs;
+
+    /**
+     * 连接池集合.
+     *
+     * @var SwooleHttp2ConnectionPool[]
+     */
+    private $connectionPools = [];
+
+    /**
+     * @var static
+     */
+    private static $instance;
+
+    /**
+     * @return static
+     */
+    public static function getInstance()
+    {
+        if (null === self::$instance)
+        {
+            return self::$instance = new static();
+        }
+
+        return self::$instance;
+    }
+
+    /**
+     * 获取连接池数组.
+     *
+     * @return IConnectionPool[]
+     */
+    public function getConnectionPools()
+    {
+        return $this->connectionPools;
+    }
+
+    /**
+     * 获取连接池对象
+     *
+     * @param string $url
+     *
+     * @return IConnectionPool
+     */
+    public function getConnectionPool($url)
+    {
+        if (isset($this->connectionPools[$url]))
+        {
+            return $this->connectionPools[$url];
+        }
+        else
+        {
+            $config = ConnectionPool::getConfig($url);
+            if (null === $config)
+            {
+                ConnectionPool::setConfig($url);
+            }
+            $config = ConnectionPool::getConfig($url);
+
+            return $this->connectionPools[$url] = new SwooleHttp2ConnectionPool($config);
+        }
+    }
+
+    /**
+     * 获取连接.
+     *
+     * @param string $url
+     *
+     * @return mixed
+     */
+    public function getConnection($url)
+    {
+        $connectionPool = $this->getConnectionPool($url);
+
+        return $connectionPool->getConnection();
+    }
+
+    /**
+     * 释放连接占用.
+     *
+     * @param string $url
+     * @param mixed  $connection
+     *
+     * @return void
+     */
+    public function release($url, $connection)
+    {
+        $connectionPool = $this->getConnectionPool($url);
+        $connectionPool->release($connection);
+    }
+
+    /**
+     * 关闭指定连接.
+     *
+     * @param string $url
+     *
+     * @return bool
+     */
+    public function closeConnection($url)
+    {
+        return false;
+    }
+
+    /**
+     * 创建新连接,但不归本管理器管理.
+     *
+     * @param string $url
+     *
+     * @return mixed
+     */
+    public function createConnection($url)
+    {
+        $connectionPool = $this->getConnectionPool($url);
+
+        return $connectionPool->createConnection();
+    }
+
+    /**
+     * 关闭连接管理器.
+     *
+     * @return void
+     */
+    public function close()
+    {
+        $connectionPools = $this->connectionPools;
+        $this->connectionPools = [];
+        foreach ($connectionPools as $connectionPool)
+        {
+            $connectionPool->close();
+        }
+    }
+}

+ 135 - 0
vendor/yurunsoft/yurun-http/src/YurunHttp/Handler/Swoole/SwooleHttp2ConnectionPool.php

@@ -0,0 +1,135 @@
+<?php
+
+namespace Yurun\Util\YurunHttp\Handler\Swoole;
+
+use Swoole\Coroutine\Channel;
+use Swoole\Coroutine\Http2\Client;
+use Yurun\Util\YurunHttp\Http\Psr7\Uri;
+use Yurun\Util\YurunHttp\Pool\BaseConnectionPool;
+
+class SwooleHttp2ConnectionPool extends BaseConnectionPool
+{
+    /**
+     * 队列.
+     *
+     * @var Channel
+     */
+    protected $channel;
+
+    /**
+     * 连接数组.
+     *
+     * @var Client[]
+     */
+    protected $connections = [];
+
+    /**
+     * @param \Yurun\Util\YurunHttp\Pool\Config\PoolConfig $config
+     */
+    public function __construct($config)
+    {
+        parent::__construct($config);
+        $this->channel = new Channel(1024);
+    }
+
+    /**
+     * 关闭连接池和连接池中的连接.
+     *
+     * @return void
+     */
+    public function close()
+    {
+        $connections = $this->connections;
+        $this->connections = [];
+        $this->channel = new Channel(1024);
+        foreach ($connections as $connection)
+        {
+            $connection->close();
+        }
+    }
+
+    /**
+     * 创建一个连接,但不受连接池管理.
+     *
+     * @return mixed
+     */
+    public function createConnection()
+    {
+        $config = $this->config;
+        $uri = new Uri($config->getUrl());
+        $scheme = $uri->getScheme();
+        $client = new Client($uri->getHost(), Uri::getServerPort($uri), 'https' === $scheme || 'wss' === $scheme);
+        if ($client->connect())
+        {
+            return $client;
+        }
+        else
+        {
+            throw new \RuntimeException(sprintf('Http2 connect failed! errCode: %s, errMsg:%s', $client->errCode, swoole_strerror($client->errCode)));
+        }
+    }
+
+    /**
+     * 获取连接.
+     *
+     * @return mixed
+     */
+    public function getConnection()
+    {
+        $config = $this->getConfig();
+        $maxConnections = $this->getConfig()->getMaxConnections();
+        if ($this->getFree() > 0 || (0 != $maxConnections && $this->getCount() >= $maxConnections))
+        {
+            return $this->channel->pop($config->getWaitTimeout());
+        }
+        else
+        {
+            return $this->connections[] = $this->createConnection();
+        }
+    }
+
+    /**
+     * 释放连接占用.
+     *
+     * @param mixed $connection
+     *
+     * @return void
+     */
+    public function release($connection)
+    {
+        if (\in_array($connection, $this->connections))
+        {
+            $this->channel->push($connection);
+        }
+    }
+
+    /**
+     * 获取当前池子中连接总数.
+     *
+     * @return int
+     */
+    public function getCount()
+    {
+        return \count($this->connections);
+    }
+
+    /**
+     * 获取当前池子中空闲连接总数.
+     *
+     * @return int
+     */
+    public function getFree()
+    {
+        return $this->channel->length();
+    }
+
+    /**
+     * 获取当前池子中正在使用的连接总数.
+     *
+     * @return int
+     */
+    public function getUsed()
+    {
+        return $this->getCount() - $this->getFree();
+    }
+}

+ 143 - 0
vendor/yurunsoft/yurun-http/src/YurunHttp/Handler/Swoole/SwooleHttpConnectionManager.php

@@ -0,0 +1,143 @@
+<?php
+
+namespace Yurun\Util\YurunHttp\Handler\Swoole;
+
+use Yurun\Util\YurunHttp\ConnectionPool;
+use Yurun\Util\YurunHttp\Handler\Contract\IConnectionManager;
+use Yurun\Util\YurunHttp\Pool\Contract\IConnectionPool;
+use Yurun\Util\YurunHttp\Pool\Traits\TConnectionPoolConfigs;
+
+class SwooleHttpConnectionManager implements IConnectionManager
+{
+    use TConnectionPoolConfigs;
+
+    /**
+     * 连接池集合.
+     *
+     * @var SwooleHttpConnectionPool[]
+     */
+    private $connectionPools = [];
+
+    /**
+     * @var static
+     */
+    private static $instance;
+
+    /**
+     * @return static
+     */
+    public static function getInstance()
+    {
+        if (null === self::$instance)
+        {
+            return self::$instance = new static();
+        }
+
+        return self::$instance;
+    }
+
+    /**
+     * 获取连接池数组.
+     *
+     * @return IConnectionPool[]
+     */
+    public function getConnectionPools()
+    {
+        return $this->connectionPools;
+    }
+
+    /**
+     * 获取连接池对象
+     *
+     * @param string $url
+     *
+     * @return IConnectionPool
+     */
+    public function getConnectionPool($url)
+    {
+        if (isset($this->connectionPools[$url]))
+        {
+            return $this->connectionPools[$url];
+        }
+        else
+        {
+            $config = ConnectionPool::getConfig($url);
+            if (null === $config)
+            {
+                ConnectionPool::setConfig($url);
+            }
+            $config = ConnectionPool::getConfig($url);
+
+            return $this->connectionPools[$url] = new SwooleHttpConnectionPool($config);
+        }
+    }
+
+    /**
+     * 获取连接.
+     *
+     * @param string $url
+     *
+     * @return mixed
+     */
+    public function getConnection($url)
+    {
+        $connectionPool = $this->getConnectionPool($url);
+
+        return $connectionPool->getConnection();
+    }
+
+    /**
+     * 释放连接占用.
+     *
+     * @param string $url
+     * @param mixed  $connection
+     *
+     * @return void
+     */
+    public function release($url, $connection)
+    {
+        $connectionPool = $this->getConnectionPool($url);
+        $connectionPool->release($connection);
+    }
+
+    /**
+     * 关闭指定连接.
+     *
+     * @param string $url
+     *
+     * @return bool
+     */
+    public function closeConnection($url)
+    {
+        return false;
+    }
+
+    /**
+     * 创建新连接,但不归本管理器管理.
+     *
+     * @param string $url
+     *
+     * @return mixed
+     */
+    public function createConnection($url)
+    {
+        $connectionPool = $this->getConnectionPool($url);
+
+        return $connectionPool->createConnection();
+    }
+
+    /**
+     * 关闭连接管理器.
+     *
+     * @return void
+     */
+    public function close()
+    {
+        $connectionPools = $this->connectionPools;
+        $this->connectionPools = [];
+        foreach ($connectionPools as $connectionPool)
+        {
+            $connectionPool->close();
+        }
+    }
+}

+ 128 - 0
vendor/yurunsoft/yurun-http/src/YurunHttp/Handler/Swoole/SwooleHttpConnectionPool.php

@@ -0,0 +1,128 @@
+<?php
+
+namespace Yurun\Util\YurunHttp\Handler\Swoole;
+
+use Swoole\Coroutine\Channel;
+use Swoole\Coroutine\Http\Client;
+use Yurun\Util\YurunHttp\Http\Psr7\Uri;
+use Yurun\Util\YurunHttp\Pool\BaseConnectionPool;
+
+class SwooleHttpConnectionPool extends BaseConnectionPool
+{
+    /**
+     * 队列.
+     *
+     * @var Channel
+     */
+    protected $channel;
+
+    /**
+     * 连接数组.
+     *
+     * @var Client[]
+     */
+    protected $connections = [];
+
+    /**
+     * @param \Yurun\Util\YurunHttp\Pool\Config\PoolConfig $config
+     */
+    public function __construct($config)
+    {
+        parent::__construct($config);
+        $this->channel = new Channel(1024);
+    }
+
+    /**
+     * 关闭连接池和连接池中的连接.
+     *
+     * @return void
+     */
+    public function close()
+    {
+        $connections = $this->connections;
+        $this->connections = [];
+        $this->channel = new Channel(1024);
+        foreach ($connections as $connection)
+        {
+            $connection->close();
+        }
+    }
+
+    /**
+     * 创建一个连接,但不受连接池管理.
+     *
+     * @return mixed
+     */
+    public function createConnection()
+    {
+        $config = $this->config;
+        $uri = new Uri($config->getUrl());
+        $scheme = $uri->getScheme();
+
+        return new Client($uri->getHost(), Uri::getServerPort($uri), 'https' === $scheme || 'wss' === $scheme);
+    }
+
+    /**
+     * 获取连接.
+     *
+     * @return mixed
+     */
+    public function getConnection()
+    {
+        $config = $this->getConfig();
+        $maxConnections = $this->getConfig()->getMaxConnections();
+        if ($this->getFree() > 0 || (0 != $maxConnections && $this->getCount() >= $maxConnections))
+        {
+            return $this->channel->pop($config->getWaitTimeout());
+        }
+        else
+        {
+            return $this->connections[] = $this->createConnection();
+        }
+    }
+
+    /**
+     * 释放连接占用.
+     *
+     * @param mixed $connection
+     *
+     * @return void
+     */
+    public function release($connection)
+    {
+        if (\in_array($connection, $this->connections))
+        {
+            $this->channel->push($connection);
+        }
+    }
+
+    /**
+     * 获取当前池子中连接总数.
+     *
+     * @return int
+     */
+    public function getCount()
+    {
+        return \count($this->connections);
+    }
+
+    /**
+     * 获取当前池子中空闲连接总数.
+     *
+     * @return int
+     */
+    public function getFree()
+    {
+        return $this->channel->length();
+    }
+
+    /**
+     * 获取当前池子中正在使用的连接总数.
+     *
+     * @return int
+     */
+    public function getUsed()
+    {
+        return $this->getCount() - $this->getFree();
+    }
+}

+ 396 - 0
vendor/yurunsoft/yurun-http/src/YurunHttp/Http/Psr7/AbstractMessage.php

@@ -0,0 +1,396 @@
+<?php
+
+namespace Yurun\Util\YurunHttp\Http\Psr7;
+
+use Psr\Http\Message\MessageInterface;
+use Psr\Http\Message\StreamInterface;
+use Yurun\Util\YurunHttp\Stream\MemoryStream;
+
+abstract class AbstractMessage implements MessageInterface
+{
+    /**
+     * Http协议版本.
+     *
+     * @var string
+     */
+    protected $protocolVersion = '1.1';
+
+    /**
+     * 头.
+     *
+     * @var array
+     */
+    protected $headers = [];
+
+    /**
+     * 头名称数组
+     * 小写的头 => 第一次使用的头名称.
+     *
+     * @var array
+     */
+    protected $headerNames = [];
+
+    /**
+     * 消息主体.
+     *
+     * @var \Psr\Http\Message\StreamInterface
+     */
+    protected $body;
+
+    /**
+     * @param string|\Psr\Http\Message\StreamInterface $body
+     */
+    public function __construct($body)
+    {
+        if ($body instanceof \Psr\Http\Message\StreamInterface)
+        {
+            $this->body = $body;
+        }
+        else
+        {
+            $this->body = new MemoryStream($body);
+        }
+    }
+
+    /**
+     * Retrieves the HTTP protocol version as a string.
+     *
+     * The string MUST contain only the HTTP version number (e.g., "1.1", "1.0").
+     *
+     * @return string HTTP protocol version
+     */
+    public function getProtocolVersion()
+    {
+        return $this->protocolVersion;
+    }
+
+    /**
+     * Return an instance with the specified HTTP protocol version.
+     *
+     * The version string MUST contain only the HTTP version number (e.g.,
+     * "1.1", "1.0").
+     *
+     * This method MUST be implemented in such a way as to retain the
+     * immutability of the message, and MUST return an instance that has the
+     * new protocol version.
+     *
+     * @param string $version HTTP protocol version
+     *
+     * @return static
+     */
+    public function withProtocolVersion($version)
+    {
+        $self = clone $this;
+        $self->protocolVersion = $version;
+
+        return $self;
+    }
+
+    /**
+     * Retrieves all message header values.
+     *
+     * The keys represent the header name as it will be sent over the wire, and
+     * each value is an array of strings associated with the header.
+     *
+     *     // Represent the headers as a string
+     *     foreach ($message->getHeaders() as $name => $values) {
+     *         echo $name . ": " . implode(", ", $values);
+     *     }
+     *
+     *     // Emit headers iteratively:
+     *     foreach ($message->getHeaders() as $name => $values) {
+     *         foreach ($values as $value) {
+     *             header(sprintf('%s: %s', $name, $value), false);
+     *         }
+     *     }
+     *
+     * While header names are not case-sensitive, getHeaders() will preserve the
+     * exact case in which headers were originally specified.
+     *
+     * @return array Returns an associative array of the message's headers. Each
+     *               key MUST be a header name, and each value MUST be an array of strings
+     *               for that header.
+     */
+    public function getHeaders()
+    {
+        return $this->headers;
+    }
+
+    /**
+     * Checks if a header exists by the given case-insensitive name.
+     *
+     * @param string $name case-insensitive header field name
+     *
+     * @return bool Returns true if any header names match the given header
+     *              name using a case-insensitive string comparison. Returns false if
+     *              no matching header name is found in the message.
+     */
+    public function hasHeader($name)
+    {
+        $lowerName = strtolower($name);
+        if (isset($this->headerNames[$lowerName]))
+        {
+            $name = $this->headerNames[$lowerName];
+        }
+
+        return isset($this->headers[$name]);
+    }
+
+    /**
+     * Retrieves a message header value by the given case-insensitive name.
+     *
+     * This method returns an array of all the header values of the given
+     * case-insensitive header name.
+     *
+     * If the header does not appear in the message, this method MUST return an
+     * empty array.
+     *
+     * @param string $name case-insensitive header field name
+     *
+     * @return string[] An array of string values as provided for the given
+     *                  header. If the header does not appear in the message, this method MUST
+     *                  return an empty array.
+     */
+    public function getHeader($name)
+    {
+        $lowerName = strtolower($name);
+        if (isset($this->headerNames[$lowerName]))
+        {
+            $name = $this->headerNames[$lowerName];
+        }
+        if (isset($this->headers[$name]))
+        {
+            return $this->headers[$name];
+        }
+        else
+        {
+            return [];
+        }
+    }
+
+    /**
+     * Retrieves a comma-separated string of the values for a single header.
+     *
+     * This method returns all of the header values of the given
+     * case-insensitive header name as a string concatenated together using
+     * a comma.
+     *
+     * NOTE: Not all header values may be appropriately represented using
+     * comma concatenation. For such headers, use getHeader() instead
+     * and supply your own delimiter when concatenating.
+     *
+     * If the header does not appear in the message, this method MUST return
+     * an empty string.
+     *
+     * @param string $name case-insensitive header field name
+     *
+     * @return string A string of values as provided for the given header
+     *                concatenated together using a comma. If the header does not appear in
+     *                the message, this method MUST return an empty string.
+     */
+    public function getHeaderLine($name)
+    {
+        $lowerName = strtolower($name);
+        if (isset($this->headerNames[$lowerName]))
+        {
+            $name = $this->headerNames[$lowerName];
+        }
+        if (!isset($this->headers[$name]))
+        {
+            return '';
+        }
+
+        return implode(',', $this->headers[$name]);
+    }
+
+    /**
+     * Return an instance with the provided value replacing the specified header.
+     *
+     * While header names are case-insensitive, the casing of the header will
+     * be preserved by this function, and returned from getHeaders().
+     *
+     * This method MUST be implemented in such a way as to retain the
+     * immutability of the message, and MUST return an instance that has the
+     * new and/or updated header and value.
+     *
+     * @param string          $name  case-insensitive header field name
+     * @param string|string[] $value header value(s)
+     *
+     * @return static
+     *
+     * @throws \InvalidArgumentException for invalid header names or values
+     */
+    public function withHeader($name, $value)
+    {
+        $self = clone $this;
+
+        return $this->setHeader($self, $name, $value);
+    }
+
+    /**
+     * Return an instance with the specified header appended with the given value.
+     *
+     * Existing values for the specified header will be maintained. The new
+     * value(s) will be appended to the existing list. If the header did not
+     * exist previously, it will be added.
+     *
+     * This method MUST be implemented in such a way as to retain the
+     * immutability of the message, and MUST return an instance that has the
+     * new header and/or value.
+     *
+     * @param string          $name  case-insensitive header field name to add
+     * @param string|string[] $value header value(s)
+     *
+     * @return static
+     *
+     * @throws \InvalidArgumentException for invalid header names or values
+     */
+    public function withAddedHeader($name, $value)
+    {
+        $self = clone $this;
+        $lowerName = strtolower($name);
+        if (isset($self->headerNames[$lowerName]))
+        {
+            $name = $self->headerNames[$lowerName];
+        }
+        else
+        {
+            $self->headerNames[$lowerName] = $name;
+        }
+
+        if (\is_string($value))
+        {
+            $value = [$value];
+        }
+        elseif (!\is_array($value))
+        {
+            throw new \InvalidArgumentException('invalid header names or values');
+        }
+
+        if (isset($self->headers[$name]))
+        {
+            $self->headers[$name] = array_merge($self->headers[$name], $value);
+        }
+        else
+        {
+            $self->headers[$name] = $value;
+        }
+
+        return $self;
+    }
+
+    /**
+     * Return an instance without the specified header.
+     *
+     * Header resolution MUST be done without case-sensitivity.
+     *
+     * This method MUST be implemented in such a way as to retain the
+     * immutability of the message, and MUST return an instance that removes
+     * the named header.
+     *
+     * @param string $name case-insensitive header field name to remove
+     *
+     * @return static
+     */
+    public function withoutHeader($name)
+    {
+        $self = clone $this;
+        $lowerName = strtolower($name);
+        if (isset($self->headerNames[$lowerName]))
+        {
+            $name = $self->headerNames[$lowerName];
+        }
+        if (isset($self->headers[$name]))
+        {
+            unset($self->headers[$name]);
+        }
+
+        return $self;
+    }
+
+    /**
+     * Gets the body of the message.
+     *
+     * @return StreamInterface returns the body as a stream
+     */
+    public function getBody()
+    {
+        return $this->body;
+    }
+
+    /**
+     * Return an instance with the specified message body.
+     *
+     * The body MUST be a StreamInterface object.
+     *
+     * This method MUST be implemented in such a way as to retain the
+     * immutability of the message, and MUST return a new instance that has the
+     * new body stream.
+     *
+     * @param StreamInterface $body body
+     *
+     * @return static
+     *
+     * @throws \InvalidArgumentException when the body is not valid
+     */
+    public function withBody(StreamInterface $body)
+    {
+        $self = clone $this;
+        $self->body = $body;
+
+        return $self;
+    }
+
+    /**
+     * 在当前实例下设置头.
+     *
+     * @param array $headers
+     *
+     * @return static
+     */
+    protected function setHeaders(array $headers)
+    {
+        foreach ($headers as $name => $value)
+        {
+            $this->setHeader($this, $name, $value);
+        }
+
+        return $this;
+    }
+
+    /**
+     * 设置header.
+     *
+     * @param static $object
+     * @param string $name
+     * @param string $value
+     *
+     * @return static
+     */
+    protected function setHeader($object, $name, $value)
+    {
+        $lowerName = strtolower($name);
+        if (isset($object->headerNames[$lowerName]))
+        {
+            $name = $object->headerNames[$lowerName];
+        }
+        else
+        {
+            $object->headerNames[$lowerName] = $name;
+        }
+        if (\is_string($value))
+        {
+            $object->headers[$name] = [$value];
+        }
+        elseif (\is_array($value))
+        {
+            $object->headers[$name] = $value;
+        }
+        else
+        {
+            throw new \InvalidArgumentException('invalid header names or values');
+        }
+
+        return $object;
+    }
+}

+ 211 - 0
vendor/yurunsoft/yurun-http/src/YurunHttp/Http/Psr7/Consts/MediaType.php

@@ -0,0 +1,211 @@
+<?php
+
+namespace Yurun\Util\YurunHttp\Http\Psr7\Consts;
+
+/**
+ * 常见的媒体类型.
+ */
+abstract class MediaType
+{
+    const ALL = '*/*';
+
+    const APPLICATION_ATOM_XML = 'application/atom+xml';
+
+    const APPLICATION_FORM_URLENCODED = 'application/x-www-form-urlencoded';
+
+    const APPLICATION_JSON = 'application/json';
+
+    const APPLICATION_JSON_UTF8 = 'application/json;charset=UTF-8';
+
+    const APPLICATION_OCTET_STREAM = 'application/octet-stream';
+
+    const APPLICATION_PDF = 'application/pdf';
+
+    const APPLICATION_PROBLEM_JSON = 'application/problem+json';
+
+    const APPLICATION_PROBLEM_XML = 'application/problem+xml';
+
+    const APPLICATION_RSS_XML = 'application/rss+xml';
+
+    const APPLICATION_STREAM_JSON = 'application/stream+json';
+
+    const APPLICATION_XHTML_XML = 'application/xhtml+xml';
+
+    const APPLICATION_XML = 'application/xml';
+
+    const IMAGE_JPEG = 'image/jpeg';
+
+    const IMAGE_APNG = 'image/apng';
+
+    const IMAGE_PNG = 'image/png';
+
+    const IMAGE_GIF = 'image/gif';
+
+    const IMAGE_WEBP = 'image/webp';
+
+    const MULTIPART_FORM_DATA = 'multipart/form-data';
+
+    const TEXT_EVENT_STREAM = 'text/event-stream';
+
+    const TEXT_HTML = 'text/html';
+
+    const TEXT_MARKDOWN = 'text/markdown';
+
+    const TEXT_PLAIN = 'text/plain';
+
+    const TEXT_XML = 'text/xml';
+
+    /**
+     * @var array
+     */
+    private static $extMap = [
+        'Type/sub-type'                                                             => 'Extension',
+        'text/h323'                                                                 => '323',
+        'application/internet-property-stream'                                      => 'acx',
+        'application/postscript'                                                    => 'ai',
+        'audio/x-aiff'                                                              => 'aiff',
+        'video/x-ms-asf'                                                            => 'asf',
+        'audio/basic'                                                               => 'au',
+        'video/x-msvideo'                                                           => 'avi',
+        'application/olescript'                                                     => 'axs',
+        'application/x-bcpio'                                                       => 'bcpio',
+        'image/bmp'                                                                 => 'bmp',
+        'application/vnd.ms-pkiseccat'                                              => 'cat',
+        'application/x-cdf'                                                         => 'cdf',
+        'application/x-msclip'                                                      => 'clp',
+        'image/x-cmx'                                                               => 'cmx',
+        'image/cis-cod'                                                             => 'cod',
+        'application/x-cpio'                                                        => 'cpio',
+        'application/x-mscardfile'                                                  => 'crd',
+        'application/pkix-crl'                                                      => 'crl',
+        'application/x-x509-ca-cert'                                                => 'crt',
+        'application/x-csh'                                                         => 'csh',
+        'text/css'                                                                  => 'css',
+        'application/x-msdownload'                                                  => 'dll',
+        'application/msword'                                                        => 'doc',
+        'application/vnd.openxmlformats-officedocument.wordprocessingml.document'   => 'docx',
+        'application/vnd.openxmlformats-officedocument.wordprocessingml.template'   => 'dotx',
+        'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'         => 'xlsx',
+        'application/vnd.openxmlformats-officedocument.presentationml.presentation' => 'pptx',
+        'application/x-dvi'                                                         => 'dvi',
+        'application/x-director'                                                    => 'dxr',
+        'text/x-setext'                                                             => 'etx',
+        'application/envoy'                                                         => 'evy',
+        'application/fractals'                                                      => 'fif',
+        'image/gif'                                                                 => 'gif',
+        'application/x-gtar'                                                        => 'gtar',
+        'application/x-gzip'                                                        => 'gz',
+        'application/x-hdf'                                                         => 'hdf',
+        'application/winhlp'                                                        => 'hlp',
+        'application/mac-binhex40'                                                  => 'hqx',
+        'application/hta'                                                           => 'hta',
+        'text/x-component'                                                          => 'htc',
+        'text/html'                                                                 => 'html',
+        'text/webviewhtml'                                                          => 'htt',
+        'image/x-icon'                                                              => 'ico',
+        'image/ief'                                                                 => 'ief',
+        'application/x-iphone'                                                      => 'iii',
+        'image/pipeg'                                                               => 'jfif',
+        'image/jpeg'                                                                => 'jpg',
+        'application/x-javascript'                                                  => 'js',
+        'application/x-latex'                                                       => 'latex',
+        'video/x-la-asf'                                                            => 'lsf',
+        'audio/x-mpegurl'                                                           => 'm3u',
+        'application/x-troff-man'                                                   => 'man',
+        'application/x-msaccess'                                                    => 'mdb',
+        'application/x-troff-me'                                                    => 'me',
+        'message/rfc822'                                                            => 'mhtml',
+        'audio/mid'                                                                 => 'mid',
+        'application/x-msmoney'                                                     => 'mny',
+        'video/quicktime'                                                           => 'mov',
+        'video/x-sgi-movie'                                                         => 'movie',
+        'video/mpeg'                                                                => 'mpeg',
+        'application/vnd.ms-project'                                                => 'mpp',
+        'application/x-troff-ms'                                                    => 'ms',
+        'application/x-msmediaview'                                                 => 'mvb',
+        'application/oda'                                                           => 'oda',
+        'application/pkcs10'                                                        => 'p10',
+        'application/x-pkcs7-mime'                                                  => 'p7m',
+        'application/x-pkcs7-certreqresp'                                           => 'p7r',
+        'application/x-pkcs7-signature'                                             => 'p7s',
+        'image/x-portable-bitmap'                                                   => 'pbm',
+        'application/pdf'                                                           => 'pdf',
+        'application/x-pkcs12'                                                      => 'pfx',
+        'image/x-portable-graymap'                                                  => 'pgm',
+        'application/ynd.ms-pkipko'                                                 => 'pko',
+        'image/x-portable-anymap'                                                   => 'pnm',
+        'image/x-portable-pixmap'                                                   => 'ppm',
+        'application/vnd.ms-powerpoint'                                             => 'ppt',
+        'application/pics-rules'                                                    => 'prf',
+        'application/x-mspublisher'                                                 => 'pub',
+        'audio/x-pn-realaudio'                                                      => 'ram',
+        'image/x-cmu-raster'                                                        => 'ras',
+        'image/x-rgb'                                                               => 'rgb',
+        'application/rtf'                                                           => 'rtf',
+        'text/richtext'                                                             => 'rtx',
+        'application/x-msschedule'                                                  => 'scd',
+        'text/scriptlet'                                                            => 'sct',
+        'application/set-payment-initiation'                                        => 'setpay',
+        'application/set-registration-initiation'                                   => 'setreg',
+        'application/x-sh'                                                          => 'sh',
+        'application/x-shar'                                                        => 'shar',
+        'application/x-stuffit'                                                     => 'sit',
+        'application/x-pkcs7-certificates'                                          => 'spc',
+        'application/futuresplash'                                                  => 'spl',
+        'application/x-wais-source'                                                 => 'src',
+        'application/vnd.ms-pkicertstore'                                           => 'sst',
+        'application/vnd.ms-pkistl'                                                 => 'stl',
+        'image/svg+xml'                                                             => 'svg',
+        'application/x-sv4cpio'                                                     => 'sv4cpio',
+        'application/x-sv4crc'                                                      => 'sv4crc',
+        'application/x-shockwave-flash'                                             => 'swf',
+        'application/x-tar'                                                         => 'tar',
+        'application/x-tcl'                                                         => 'tcl',
+        'application/x-tex'                                                         => 'tex',
+        'application/x-texinfo'                                                     => 'texi',
+        'application/x-compressed'                                                  => 'tgz',
+        'image/tiff'                                                                => 'tiff',
+        'application/x-msterminal'                                                  => 'trm',
+        'text/tab-separated-values'                                                 => 'tsv',
+        'text/plain'                                                                => 'txt',
+        'text/iuls'                                                                 => 'uls',
+        'application/x-ustar'                                                       => 'ustar',
+        'text/x-vcard'                                                              => 'vcf',
+        'audio/x-wav'                                                               => 'wav',
+        'application/x-msmetafile'                                                  => 'wmf',
+        'application/x-mswrite'                                                     => 'wri',
+        'image/x-xbitmap'                                                           => 'xbm',
+        'application/vnd.ms-excel'                                                  => 'xls',
+        'image/x-xpixmap'                                                           => 'xpm',
+        'image/x-xwindowdump'                                                       => 'xwd',
+        'application/x-compress'                                                    => 'z',
+        'application/zip'                                                           => 'zip',
+        'application/vnd.android.package-archive'                                   => 'apk',
+        'application/x-silverlight-app'                                             => 'xap',
+        'application/vnd.iphone'                                                    => 'ipa',
+        'text/markdown'                                                             => 'md',
+        'text/xml'                                                                  => 'xml',
+        'image/webp'                                                                => 'webp',
+        'image/png'                                                                 => 'png',
+    ];
+
+    /**
+     * 获取 ContentType 对应的扩展名(不包含点).
+     *
+     * @param string $contentType
+     *
+     * @return string|null
+     */
+    public static function getExt($contentType)
+    {
+        list($firstContentType) = explode(';', $contentType, 2);
+        if (isset(static::$extMap[$firstContentType]))
+        {
+            return static::$extMap[$firstContentType];
+        }
+        else
+        {
+            return null;
+        }
+    }
+}

+ 42 - 0
vendor/yurunsoft/yurun-http/src/YurunHttp/Http/Psr7/Consts/RequestHeader.php

@@ -0,0 +1,42 @@
+<?php
+
+namespace Yurun\Util\YurunHttp\Http\Psr7\Consts;
+
+/**
+ * 常见的http请求头.
+ */
+abstract class RequestHeader
+{
+    const ACCEPT = 'Accept';
+    const ACCEPT_CHARSET = 'Accept-Charset';
+    const ACCEPT_ENCODING = 'Accept-Encoding';
+    const ACCEPT_LANGUAGE = 'Accept-Language';
+    const ACCEPT_DATETIME = 'Accept-Datetime';
+    const AUTHORIZATION = 'Authorization';
+    const CACHE_CONTROL = 'Cache-Control';
+    const CONNECTION = 'Connection';
+    const COOKIE = 'Cookie';
+    const CONTENT_LENGTH = 'Content-Length';
+    const CONTENT_MD5 = 'Content-MD5';
+    const CONTENT_TYPE = 'Content-Type';
+    const DATE = 'Date';
+    const EXPECT = 'Expect';
+    const FROM = 'From';
+    const HOST = 'Host';
+    const IF_MATCH = 'If-Match';
+    const IF_MODIFIED_SINCE = 'If-Modified-Since';
+    const IF_NONE_MATCH = 'If-None-Match';
+    const IF_RANGE = 'If-Range';
+    const IF_UNMODIFIED_SINCE = 'If-Unmodified-Since';
+    const MAX_FORWARDS = 'Max-Forwards';
+    const ORIGIN = 'Origin';
+    const PRAGMA = 'Pragma';
+    const PROXY_AUTHORIZATION = 'Proxy-Authorization';
+    const RANGE = 'Range';
+    const REFERER = 'Referer';
+    const TE = 'TE';
+    const USER_AGENT = 'User-Agent';
+    const UPGRADE = 'Upgrade';
+    const VIA = 'Via';
+    const WARNING = 'Warning';
+}

+ 25 - 0
vendor/yurunsoft/yurun-http/src/YurunHttp/Http/Psr7/Consts/RequestMethod.php

@@ -0,0 +1,25 @@
+<?php
+
+namespace Yurun\Util\YurunHttp\Http\Psr7\Consts;
+
+/**
+ * 常见的http请求方法.
+ */
+abstract class RequestMethod
+{
+    const GET = 'GET';
+
+    const POST = 'POST';
+
+    const HEAD = 'HEAD';
+
+    const PUT = 'PUT';
+
+    const PATCH = 'PATCH';
+
+    const DELETE = 'DELETE';
+
+    const OPTIONS = 'OPTIONS';
+
+    const TRACE = 'TRACE';
+}

+ 47 - 0
vendor/yurunsoft/yurun-http/src/YurunHttp/Http/Psr7/Consts/ResponseHeader.php

@@ -0,0 +1,47 @@
+<?php
+
+namespace Yurun\Util\YurunHttp\Http\Psr7\Consts;
+
+/**
+ * 常见的http响应头.
+ */
+abstract class ResponseHeader
+{
+    const ACCESS_CONTROL_ALLOW_ORIGIN = 'Access-Control-Allow-Origin';
+    const ACCEPT_PATCH = 'Accept-Patch';
+    const ACCEPT_RANGES = 'Accept-Ranges';
+    const AGE = 'Age';
+    const ALLOW = 'Allow';
+    const CACHE_CONTROL = 'Cache-Control';
+    const CONNECTION = 'Connection';
+    const CONTENT_DISPOSITION = 'Content-Disposition';
+    const CONTENT_ENCODING = 'Content-Encoding';
+    const CONTENT_LANGUAGE = 'Content-Language';
+    const CONTENT_LENGTH = 'Content-Length';
+    const CONTENT_LOCATION = 'Content-Location';
+    const CONTENT_MD5 = 'Content-MD5';
+    const CONTENT_RANGE = 'Content-Range';
+    const CONTENT_TYPE = 'Content-Type';
+    const DATE = 'Date';
+    const ETAG = 'ETag';
+    const EXPIRES = 'Expires';
+    const LAST_MODIFIED = 'Last-Modified';
+    const LINK = 'Link';
+    const LOCATION = 'Location';
+    const P3P = 'P3P';
+    const PRAGMA = 'Pragma';
+    const PROXY_AUTHENTICATE = 'Proxy-Authenticate';
+    const PUBLIC_KEY_PINS = 'Public-Key-Pins';
+    const REFRESH = 'Refresh';
+    const RETRY_AFTER = 'Retry-After';
+    const SERVER = 'Server';
+    const SET_COOKIE = 'Set-Cookie';
+    const STATUS = 'Status';
+    const TRAILER = 'Trailer';
+    const TRANSFER_ENCODING = 'Transfer-Encoding';
+    const UPGRADE = 'Upgrade';
+    const VARY = 'Vary';
+    const VIA = 'Via';
+    const WARNING = 'Warning';
+    const WWW_AUTHENTICATE = 'WWW-Authenticate';
+}

+ 147 - 0
vendor/yurunsoft/yurun-http/src/YurunHttp/Http/Psr7/Consts/StatusCode.php

@@ -0,0 +1,147 @@
+<?php
+
+namespace Yurun\Util\YurunHttp\Http\Psr7\Consts;
+
+abstract class StatusCode
+{
+    const _CONTINUE = 100;
+    const SWITCHING_PROTOCOLS = 101;
+    const PROCESSING = 102;
+    const OK = 200;
+    const CREATED = 201;
+    const ACCEPTED = 202;
+    const NON_AUTHORITATIVE_INFORMATION = 203;
+    const NO_CONTENT = 204;
+    const RESET_CONTENT = 205;
+    const PARTIAL_CONTENT = 206;
+    const MULTI_STATUS = 207;
+    const ALREADY_REPORTED = 208;
+    const IM_USED = 226;
+    const MULTIPLE_CHOICES = 300;
+    const MOVED_PERMANENTLY = 301;
+    const FOUND = 302;
+    const SEE_OTHER = 303;
+    const NOT_MODIFIED = 304;
+    const USE_PROXY = 305;
+    const SWITCH_PROXY = 306;
+    const TEMPORARY_REDIRECT = 307;
+    const PERMANENT_REDIRECT = 308;
+    const BAD_REQUEST = 400;
+    const UNAUTHORIZED = 401;
+    const PAYMENT_REQUIRED = 402;
+    const FORBIDDEN = 403;
+    const NOT_FOUND = 404;
+    const METHOD_NOT_ALLOWED = 405;
+    const NOT_ACCEPTABLE = 406;
+    const PROXY_AUTHENTICATION_REQUIRED = 407;
+    const REQUEST_TIME_OUT = 408;
+    const CONFLICT = 409;
+    const GONE = 410;
+    const LENGTH_REQUIRED = 411;
+    const PRECONDITION_FAILED = 412;
+    const REQUEST_ENTITY_TOO_LARGE = 413;
+    const REQUEST_URI_TOO_LARGE = 414;
+    const UNSUPPORTED_MEDIA_TYPE = 415;
+    const REQUESTED_RANGE_NOT_SATISFIABLE = 416;
+    const EXPECTATION_FAILED = 417;
+    const MISDIRECTED_REQUEST = 421;
+    const UNPROCESSABLE_ENTITY = 422;
+    const LOCKED = 423;
+    const FAILED_DEPENDENCY = 424;
+    const UNORDERED_COLLECTION = 425;
+    const UPGRADE_REQUIRED = 426;
+    const PRECONDITION_REQUIRED = 428;
+    const TOO_MANY_REQUESTS = 429;
+    const REQUEST_HEADER_FIELDS_TOO_LARGE = 431;
+    const UNAVAILABLE_FOR_LEGAL_REASONS = 451;
+    const INTERNAL_SERVER_ERROR = 500;
+    const NOT_IMPLEMENTED = 501;
+    const BAD_GATEWAY = 502;
+    const SERVICE_UNAVAILABLE = 503;
+    const GATEWAY_TIME_OUT = 504;
+    const HTTP_VERSION_NOT_SUPPORTED = 505;
+    const VARIANT_ALSO_NEGOTIATES = 506;
+    const INSUFFICIENT_STORAGE = 507;
+    const LOOP_DETECTED = 508;
+    const NOT_EXTENDED = 510;
+    const NETWORK_AUTHENTICATION_REQUIRED = 511;
+
+    /**
+     * @var array
+     */
+    protected static $reasonPhrases = [
+        self::_CONTINUE                       => 'Continue',
+        self::SWITCHING_PROTOCOLS             => 'Switching Protocols',
+        self::PROCESSING                      => 'Processing',
+        self::OK                              => 'OK',
+        self::CREATED                         => 'Created',
+        self::ACCEPTED                        => 'Accepted',
+        self::NON_AUTHORITATIVE_INFORMATION   => 'Non-Authoritative Information',
+        self::NO_CONTENT                      => 'No Content',
+        self::RESET_CONTENT                   => 'Reset Content',
+        self::PARTIAL_CONTENT                 => 'Partial Content',
+        self::MULTI_STATUS                    => 'Multi-status',
+        self::ALREADY_REPORTED                => 'Already Reported',
+        self::IM_USED                         => 'IM Used',
+        self::MULTIPLE_CHOICES                => 'Multiple Choices',
+        self::MOVED_PERMANENTLY               => 'Moved Permanently',
+        self::FOUND                           => 'Found',
+        self::SEE_OTHER                       => 'See Other',
+        self::NOT_MODIFIED                    => 'Not Modified',
+        self::USE_PROXY                       => 'Use Proxy',
+        self::SWITCH_PROXY                    => 'Switch Proxy',
+        self::TEMPORARY_REDIRECT              => 'Temporary Redirect',
+        self::PERMANENT_REDIRECT              => 'Permanent Redirect',
+        self::BAD_REQUEST                     => 'Bad Request',
+        self::UNAUTHORIZED                    => 'Unauthorized',
+        self::PAYMENT_REQUIRED                => 'Payment Required',
+        self::FORBIDDEN                       => 'Forbidden',
+        self::NOT_FOUND                       => 'Not Found',
+        self::METHOD_NOT_ALLOWED              => 'Method Not Allowed',
+        self::NOT_ACCEPTABLE                  => 'Not Acceptable',
+        self::PROXY_AUTHENTICATION_REQUIRED   => 'Proxy Authentication Required',
+        self::REQUEST_TIME_OUT                => 'Request Time-out',
+        self::CONFLICT                        => 'Conflict',
+        self::GONE                            => 'Gone',
+        self::LENGTH_REQUIRED                 => 'Length Required',
+        self::PRECONDITION_FAILED             => 'Precondition Failed',
+        self::REQUEST_ENTITY_TOO_LARGE        => 'Request Entity Too Large',
+        self::REQUEST_URI_TOO_LARGE           => 'Request-URI Too Large',
+        self::UNSUPPORTED_MEDIA_TYPE          => 'Unsupported Media Type',
+        self::REQUESTED_RANGE_NOT_SATISFIABLE => 'Requested range not satisfiable',
+        self::EXPECTATION_FAILED              => 'Expectation Failed',
+        self::MISDIRECTED_REQUEST             => 'Misdirected Request',
+        self::UNPROCESSABLE_ENTITY            => 'Unprocessable Entity',
+        self::LOCKED                          => 'Locked',
+        self::FAILED_DEPENDENCY               => 'Failed Dependency',
+        self::UNORDERED_COLLECTION            => 'Unordered Collection',
+        self::UPGRADE_REQUIRED                => 'Upgrade Required',
+        self::PRECONDITION_REQUIRED           => 'Precondition Required',
+        self::TOO_MANY_REQUESTS               => 'Too Many Requests',
+        self::REQUEST_HEADER_FIELDS_TOO_LARGE => 'Request Header Fields Too Large',
+        self::UNAVAILABLE_FOR_LEGAL_REASONS   => 'Unavailable For Legal Reasons',
+        self::INTERNAL_SERVER_ERROR           => 'Internal Server Error',
+        self::NOT_IMPLEMENTED                 => 'Not Implemented',
+        self::BAD_GATEWAY                     => 'Bad Gateway',
+        self::SERVICE_UNAVAILABLE             => 'Service Unavailable',
+        self::GATEWAY_TIME_OUT                => 'Gateway Time-out',
+        self::HTTP_VERSION_NOT_SUPPORTED      => 'HTTP Version not supported',
+        self::VARIANT_ALSO_NEGOTIATES         => 'Variant Also Negotiates',
+        self::INSUFFICIENT_STORAGE            => 'Insufficient Storage',
+        self::LOOP_DETECTED                   => 'Loop Detected',
+        self::NOT_EXTENDED                    => 'Not Extended',
+        self::NETWORK_AUTHENTICATION_REQUIRED => 'Network Authentication Required',
+    ];
+
+    /**
+     * 根据状态码获取原因短语.
+     *
+     * @param int $value
+     *
+     * @return string
+     */
+    public static function getReasonPhrase($value)
+    {
+        return isset(static::$reasonPhrases[$value]) ? static::$reasonPhrases[$value] : 'Unknown';
+    }
+}

+ 199 - 0
vendor/yurunsoft/yurun-http/src/YurunHttp/Http/Psr7/Request.php

@@ -0,0 +1,199 @@
+<?php
+
+namespace Yurun\Util\YurunHttp\Http\Psr7;
+
+use Psr\Http\Message\RequestInterface;
+use Psr\Http\Message\UriInterface;
+use Yurun\Util\YurunHttp\Http\Psr7\Consts\RequestMethod;
+
+class Request extends AbstractMessage implements RequestInterface
+{
+    /**
+     * 请求地址
+     *
+     * @var UriInterface
+     */
+    protected $uri;
+
+    /**
+     * 请求目标.
+     *
+     * @var mixed
+     */
+    protected $requestTarget;
+
+    /**
+     * 请求方法.
+     *
+     * @var string
+     */
+    protected $method;
+
+    /**
+     * 构造方法.
+     *
+     * @param string|UriInterface|null $uri
+     * @param array                    $headers
+     * @param string                   $body
+     * @param string                   $method
+     * @param string                   $version
+     */
+    public function __construct($uri = null, array $headers = [], $body = '', $method = RequestMethod::GET, $version = '1.1')
+    {
+        parent::__construct($body);
+        if (!$uri instanceof UriInterface)
+        {
+            $this->uri = new Uri($uri);
+        }
+        elseif (null !== $uri)
+        {
+            $this->uri = $uri;
+        }
+        $this->setHeaders($headers);
+        $this->method = strtoupper($method);
+        $this->protocolVersion = $version;
+    }
+
+    /**
+     * Retrieves the message's request target.
+     *
+     * Retrieves the message's request-target either as it will appear (for
+     * clients), as it appeared at request (for servers), or as it was
+     * specified for the instance (see withRequestTarget()).
+     *
+     * In most cases, this will be the origin-form of the composed URI,
+     * unless a value was provided to the concrete implementation (see
+     * withRequestTarget() below).
+     *
+     * If no URI is available, and no request-target has been specifically
+     * provided, this method MUST return the string "/".
+     *
+     * @return string
+     */
+    public function getRequestTarget()
+    {
+        return null === $this->requestTarget ? (string) $this->uri : $this->requestTarget;
+    }
+
+    /**
+     * Return an instance with the specific request-target.
+     *
+     * If the request needs a non-origin-form request-target — e.g., for
+     * specifying an absolute-form, authority-form, or asterisk-form —
+     * this method may be used to create an instance with the specified
+     * request-target, verbatim.
+     *
+     * This method MUST be implemented in such a way as to retain the
+     * immutability of the message, and MUST return an instance that has the
+     * changed request target.
+     *
+     * @see http://tools.ietf.org/html/rfc7230#section-2.7 (for the various
+     *     request-target forms allowed in request messages)
+     *
+     * @param mixed $requestTarget
+     *
+     * @return static
+     */
+    public function withRequestTarget($requestTarget)
+    {
+        $self = clone $this;
+        $self->requestTarget = $requestTarget;
+
+        return $self;
+    }
+
+    /**
+     * Retrieves the HTTP method of the request.
+     *
+     * @return string returns the request method
+     */
+    public function getMethod()
+    {
+        return $this->method;
+    }
+
+    /**
+     * Return an instance with the provided HTTP method.
+     *
+     * While HTTP method names are typically all uppercase characters, HTTP
+     * method names are case-sensitive and thus implementations SHOULD NOT
+     * modify the given string.
+     *
+     * This method MUST be implemented in such a way as to retain the
+     * immutability of the message, and MUST return an instance that has the
+     * changed request method.
+     *
+     * @param string $method case-sensitive method
+     *
+     * @return static
+     *
+     * @throws \InvalidArgumentException for invalid HTTP methods
+     */
+    public function withMethod($method)
+    {
+        $self = clone $this;
+        $self->method = $method;
+
+        return $self;
+    }
+
+    /**
+     * Retrieves the URI instance.
+     *
+     * This method MUST return a UriInterface instance.
+     *
+     * @see http://tools.ietf.org/html/rfc3986#section-4.3
+     *
+     * @return UriInterface returns a UriInterface instance
+     *                      representing the URI of the request
+     */
+    public function getUri()
+    {
+        return $this->uri;
+    }
+
+    /**
+     * Returns an instance with the provided URI.
+     *
+     * This method MUST update the Host header of the returned request by
+     * default if the URI contains a host component. If the URI does not
+     * contain a host component, any pre-existing Host header MUST be carried
+     * over to the returned request.
+     *
+     * You can opt-in to preserving the original state of the Host header by
+     * setting `$preserveHost` to `true`. When `$preserveHost` is set to
+     * `true`, this method interacts with the Host header in the following ways:
+     *
+     * - If the the Host header is missing or empty, and the new URI contains
+     *   a host component, this method MUST update the Host header in the returned
+     *   request.
+     * - If the Host header is missing or empty, and the new URI does not contain a
+     *   host component, this method MUST NOT update the Host header in the returned
+     *   request.
+     * - If a Host header is present and non-empty, this method MUST NOT update
+     *   the Host header in the returned request.
+     *
+     * This method MUST be implemented in such a way as to retain the
+     * immutability of the message, and MUST return an instance that has the
+     * new UriInterface instance.
+     *
+     * @see http://tools.ietf.org/html/rfc3986#section-4.3
+     *
+     * @param UriInterface $uri          new request URI to use
+     * @param bool         $preserveHost preserve the original state of the Host header
+     *
+     * @return static
+     */
+    public function withUri(UriInterface $uri, $preserveHost = false)
+    {
+        $self = clone $this;
+        $self->uri = $uri;
+        if (!$preserveHost)
+        {
+            $self->headers = [];
+            $self->headerNames = [];
+        }
+
+        return $self;
+    }
+}

+ 106 - 0
vendor/yurunsoft/yurun-http/src/YurunHttp/Http/Psr7/Response.php

@@ -0,0 +1,106 @@
+<?php
+
+namespace Yurun\Util\YurunHttp\Http\Psr7;
+
+use Psr\Http\Message\ResponseInterface;
+use Yurun\Util\YurunHttp\Http\Psr7\Consts\StatusCode;
+
+class Response extends AbstractMessage implements ResponseInterface
+{
+    /**
+     * 状态码
+     *
+     * @var int
+     */
+    protected $statusCode;
+
+    /**
+     * 状态码原因短语.
+     *
+     * @var string
+     */
+    protected $reasonPhrase;
+
+    /**
+     * @param string|\Psr\Http\Message\StreamInterface $body
+     * @param int                                      $statusCode
+     * @param string|null                              $reasonPhrase
+     */
+    public function __construct($body = '', $statusCode = StatusCode::OK, $reasonPhrase = null)
+    {
+        parent::__construct($body);
+        $this->statusCode = $statusCode;
+        $this->reasonPhrase = null === $reasonPhrase ? StatusCode::getReasonPhrase($this->statusCode) : $reasonPhrase;
+    }
+
+    /**
+     * Gets the response status code.
+     *
+     * The status code is a 3-digit integer result code of the server's attempt
+     * to understand and satisfy the request.
+     *
+     * @return int status code
+     */
+    public function getStatusCode()
+    {
+        return $this->statusCode;
+    }
+
+    /**
+     * Return an instance with the specified status code and, optionally, reason phrase.
+     *
+     * If no reason phrase is specified, implementations MAY choose to default
+     * to the RFC 7231 or IANA recommended reason phrase for the response's
+     * status code.
+     *
+     * This method MUST be implemented in such a way as to retain the
+     * immutability of the message, and MUST return an instance that has the
+     * updated status and reason phrase.
+     *
+     * @see http://tools.ietf.org/html/rfc7231#section-6
+     * @see http://www.iana.org/assignments/http-status-codes/http-status-codes.xhtml
+     *
+     * @param int    $code         the 3-digit integer result code to set
+     * @param string $reasonPhrase the reason phrase to use with the
+     *                             provided status code; if none is provided, implementations MAY
+     *                             use the defaults as suggested in the HTTP specification
+     *
+     * @return static
+     *
+     * @throws \InvalidArgumentException for invalid status code arguments
+     */
+    public function withStatus($code, $reasonPhrase = '')
+    {
+        $self = clone $this;
+        $self->statusCode = $code;
+        if ('' === $reasonPhrase)
+        {
+            $self->reasonPhrase = StatusCode::getReasonPhrase($code);
+        }
+        else
+        {
+            $self->reasonPhrase = $reasonPhrase;
+        }
+
+        return $self;
+    }
+
+    /**
+     * Gets the response reason phrase associated with the status code.
+     *
+     * Because a reason phrase is not a required element in a response
+     * status line, the reason phrase value MAY be null. Implementations MAY
+     * choose to return the default RFC 7231 recommended reason phrase (or those
+     * listed in the IANA HTTP Status Code Registry) for the response's
+     * status code.
+     *
+     * @see http://tools.ietf.org/html/rfc7231#section-6
+     * @see http://www.iana.org/assignments/http-status-codes/http-status-codes.xhtml
+     *
+     * @return string reason phrase; must return an empty string if none present
+     */
+    public function getReasonPhrase()
+    {
+        return $this->reasonPhrase;
+    }
+}

+ 468 - 0
vendor/yurunsoft/yurun-http/src/YurunHttp/Http/Psr7/ServerRequest.php

@@ -0,0 +1,468 @@
+<?php
+
+namespace Yurun\Util\YurunHttp\Http\Psr7;
+
+use Psr\Http\Message\ServerRequestInterface;
+use Psr\Http\Message\UriInterface;
+use Yurun\Util\YurunHttp;
+use Yurun\Util\YurunHttp\Http\Psr7\Consts\MediaType;
+use Yurun\Util\YurunHttp\Http\Psr7\Consts\RequestHeader;
+use Yurun\Util\YurunHttp\Http\Psr7\Consts\RequestMethod;
+
+class ServerRequest extends Request implements ServerRequestInterface
+{
+    /**
+     * 服务器信息.
+     *
+     * @var array
+     */
+    protected $server = [];
+
+    /**
+     * cookie数据.
+     *
+     * @var array
+     */
+    protected $cookies = [];
+
+    /**
+     * get数据.
+     *
+     * @var array
+     */
+    protected $get = [];
+
+    /**
+     * post数据.
+     *
+     * @var array
+     */
+    protected $post = [];
+
+    /**
+     * 上传的文件.
+     *
+     * @var \Yurun\Util\YurunHttp\Http\Psr7\UploadedFile[]
+     */
+    protected $files = [];
+
+    /**
+     * 处理过的主体内容.
+     *
+     * @var array|object|null
+     */
+    protected $parsedBody;
+
+    /**
+     * 属性数组.
+     *
+     * @var array
+     */
+    protected $attributes = [];
+
+    /**
+     * @param string|UriInterface|null $uri
+     * @param array                    $headers
+     * @param string                   $body
+     * @param string                   $method
+     * @param string                   $version
+     * @param array                    $server
+     * @param array                    $cookies
+     * @param array                    $files
+     */
+    public function __construct($uri = null, array $headers = [], $body = '', $method = RequestMethod::GET, $version = '1.1', array $server = [], array $cookies = [], array $files = [])
+    {
+        $this->server = $server;
+        $this->cookies = $cookies;
+        parent::__construct($uri, $headers, $body, $method, $version);
+        $this->setUploadedFiles($this, $files);
+    }
+
+    /**
+     * Retrieve server parameters.
+     *
+     * Retrieves data related to the incoming request environment,
+     * typically derived from PHP's $_SERVER superglobal. The data IS NOT
+     * REQUIRED to originate from $_SERVER.
+     *
+     * @return array
+     */
+    public function getServerParams()
+    {
+        return $this->server;
+    }
+
+    /**
+     * 获取server参数.
+     *
+     * @param string $name
+     * @param mixed  $default
+     *
+     * @return string
+     */
+    public function getServerParam($name, $default = null)
+    {
+        return isset($this->server[$name]) ? $this->server[$name] : $default;
+    }
+
+    /**
+     * Retrieve cookies.
+     *
+     * Retrieves cookies sent by the client to the server.
+     *
+     * The data MUST be compatible with the structure of the $_COOKIE
+     * superglobal.
+     *
+     * @return array
+     */
+    public function getCookieParams()
+    {
+        return $this->cookies;
+    }
+
+    /**
+     * Return an instance with the specified cookies.
+     *
+     * The data IS NOT REQUIRED to come from the $_COOKIE superglobal, but MUST
+     * be compatible with the structure of $_COOKIE. Typically, this data will
+     * be injected at instantiation.
+     *
+     * This method MUST NOT update the related Cookie header of the request
+     * instance, nor related values in the server params.
+     *
+     * This method MUST be implemented in such a way as to retain the
+     * immutability of the message, and MUST return an instance that has the
+     * updated cookie values.
+     *
+     * @param array $cookies array of key/value pairs representing cookies
+     *
+     * @return static
+     */
+    public function withCookieParams(array $cookies)
+    {
+        $self = clone $this;
+        $self->cookies = $cookies;
+
+        return $self;
+    }
+
+    /**
+     * 获取cookie值
+     *
+     * @param string $name
+     * @param mixed  $default
+     *
+     * @return mixed
+     */
+    public function getCookie($name, $default = null)
+    {
+        return isset($this->cookies[$name]) ? $this->cookies[$name] : $default;
+    }
+
+    /**
+     * Retrieve query string arguments.
+     *
+     * Retrieves the deserialized query string arguments, if any.
+     *
+     * Note: the query params might not be in sync with the URI or server
+     * params. If you need to ensure you are only getting the original
+     * values, you may need to parse the query string from `getUri()->getQuery()`
+     * or from the `QUERY_STRING` server param.
+     *
+     * @return array
+     */
+    public function getQueryParams()
+    {
+        return $this->get;
+    }
+
+    /**
+     * Return an instance with the specified query string arguments.
+     *
+     * These values SHOULD remain immutable over the course of the incoming
+     * request. They MAY be injected during instantiation, such as from PHP's
+     * $_GET superglobal, or MAY be derived from some other value such as the
+     * URI. In cases where the arguments are parsed from the URI, the data
+     * MUST be compatible with what PHP's parse_str() would return for
+     * purposes of how duplicate query parameters are handled, and how nested
+     * sets are handled.
+     *
+     * Setting query string arguments MUST NOT change the URI stored by the
+     * request, nor the values in the server params.
+     *
+     * This method MUST be implemented in such a way as to retain the
+     * immutability of the message, and MUST return an instance that has the
+     * updated query string arguments.
+     *
+     * @param array $query array of query string arguments, typically from
+     *                     $_GET
+     *
+     * @return static
+     */
+    public function withQueryParams(array $query)
+    {
+        $self = clone $this;
+        $self->get = $query;
+
+        return $self;
+    }
+
+    /**
+     * Retrieve normalized file upload data.
+     *
+     * This method returns upload metadata in a normalized tree, with each leaf
+     * an instance of Psr\Http\Message\UploadedFileInterface.
+     *
+     * These values MAY be prepared from $_FILES or the message body during
+     * instantiation, or MAY be injected via withUploadedFiles().
+     *
+     * @return UploadedFile[] an array tree of UploadedFileInterface instances; an empty
+     *                        array MUST be returned if no data is present
+     */
+    public function getUploadedFiles()
+    {
+        return $this->files;
+    }
+
+    /**
+     * Create a new instance with the specified uploaded files.
+     *
+     * This method MUST be implemented in such a way as to retain the
+     * immutability of the message, and MUST return an instance that has the
+     * updated body parameters.
+     *
+     * @param array $uploadedFiles an array tree of UploadedFileInterface instances
+     *
+     * @return static
+     *
+     * @throws \InvalidArgumentException if an invalid structure is provided
+     */
+    public function withUploadedFiles(array $uploadedFiles)
+    {
+        $self = clone $this;
+
+        return $this->setUploadedFiles($self, $uploadedFiles);
+    }
+
+    /**
+     * Retrieve any parameters provided in the request body.
+     *
+     * If the request Content-Type is either application/x-www-form-urlencoded
+     * or multipart/form-data, and the request method is POST, this method MUST
+     * return the contents of $_POST.
+     *
+     * Otherwise, this method may return any results of deserializing
+     * the request body content; as parsing returns structured content, the
+     * potential types MUST be arrays or objects only. A null value indicates
+     * the absence of body content.
+     *
+     * @return array|object|null The deserialized body parameters, if any.
+     *                           These will typically be an array or object.
+     */
+    public function getParsedBody()
+    {
+        $parsedBody = &$this->parsedBody;
+        if (null === $parsedBody)
+        {
+            $body = $this->body;
+            $contentType = strtolower($this->getHeaderLine(RequestHeader::CONTENT_TYPE));
+            // post
+            if ('POST' === $this->method && \in_array($contentType, [
+                MediaType::APPLICATION_FORM_URLENCODED,
+                MediaType::MULTIPART_FORM_DATA,
+            ]))
+            {
+                $parsedBody = $this->post;
+            }
+            // json
+            elseif (\in_array($contentType, [
+                MediaType::APPLICATION_JSON,
+                MediaType::APPLICATION_JSON_UTF8,
+            ]))
+            {
+                $parsedBody = json_decode($body, true);
+            }
+            // xml
+            elseif (\in_array($contentType, [
+                MediaType::TEXT_XML,
+                MediaType::APPLICATION_ATOM_XML,
+                MediaType::APPLICATION_RSS_XML,
+                MediaType::APPLICATION_XHTML_XML,
+                MediaType::APPLICATION_XML,
+            ]))
+            {
+                $parsedBody = new \DOMDocument();
+                $parsedBody->loadXML($body);
+            }
+            // 其它
+            else
+            {
+                $parsedBody = (object) (string) $body;
+            }
+        }
+
+        return $parsedBody;
+    }
+
+    /**
+     * Return an instance with the specified body parameters.
+     *
+     * These MAY be injected during instantiation.
+     *
+     * If the request Content-Type is either application/x-www-form-urlencoded
+     * or multipart/form-data, and the request method is POST, use this method
+     * ONLY to inject the contents of $_POST.
+     *
+     * The data IS NOT REQUIRED to come from $_POST, but MUST be the results of
+     * deserializing the request body content. Deserialization/parsing returns
+     * structured data, and, as such, this method ONLY accepts arrays or objects,
+     * or a null value if nothing was available to parse.
+     *
+     * As an example, if content negotiation determines that the request data
+     * is a JSON payload, this method could be used to create a request
+     * instance with the deserialized parameters.
+     *
+     * This method MUST be implemented in such a way as to retain the
+     * immutability of the message, and MUST return an instance that has the
+     * updated body parameters.
+     *
+     * @param array|object|null $data The deserialized body data. This will
+     *                                typically be in an array or object.
+     *
+     * @return static
+     *
+     * @throws \InvalidArgumentException if an unsupported argument type is
+     *                                   provided
+     */
+    public function withParsedBody($data)
+    {
+        $self = clone $this;
+        $self->parsedBody = $data;
+
+        return $self;
+    }
+
+    /**
+     * Retrieve attributes derived from the request.
+     *
+     * The request "attributes" may be used to allow injection of any
+     * parameters derived from the request: e.g., the results of path
+     * match operations; the results of decrypting cookies; the results of
+     * deserializing non-form-encoded message bodies; etc. Attributes
+     * will be application and request specific, and CAN be mutable.
+     *
+     * @return array attributes derived from the request
+     */
+    public function getAttributes()
+    {
+        return $this->attributes;
+    }
+
+    /**
+     * Retrieve a single derived request attribute.
+     *
+     * Retrieves a single derived request attribute as described in
+     * getAttributes(). If the attribute has not been previously set, returns
+     * the default value as provided.
+     *
+     * This method obviates the need for a hasAttribute() method, as it allows
+     * specifying a default value to return if the attribute is not found.
+     *
+     * @see getAttributes()
+     *
+     * @param string $name    the attribute name
+     * @param mixed  $default default value to return if the attribute does not exist
+     *
+     * @return mixed
+     */
+    public function getAttribute($name, $default = null)
+    {
+        $attributes = $this->attributes;
+        if (\array_key_exists($name, $attributes))
+        {
+            return $attributes[$name];
+        }
+        else
+        {
+            return YurunHttp::getAttribute($name, $default);
+        }
+    }
+
+    /**
+     * Return an instance with the specified derived request attribute.
+     *
+     * This method allows setting a single derived request attribute as
+     * described in getAttributes().
+     *
+     * This method MUST be implemented in such a way as to retain the
+     * immutability of the message, and MUST return an instance that has the
+     * updated attribute.
+     *
+     * @see getAttributes()
+     *
+     * @param string $name  the attribute name
+     * @param mixed  $value the value of the attribute
+     *
+     * @return static
+     */
+    public function withAttribute($name, $value)
+    {
+        $self = clone $this;
+        $self->attributes[$name] = $value;
+
+        return $self;
+    }
+
+    /**
+     * Return an instance that removes the specified derived request attribute.
+     *
+     * This method allows removing a single derived request attribute as
+     * described in getAttributes().
+     *
+     * This method MUST be implemented in such a way as to retain the
+     * immutability of the message, and MUST return an instance that removes
+     * the attribute.
+     *
+     * @see getAttributes()
+     *
+     * @param string $name the attribute name
+     *
+     * @return static
+     */
+    public function withoutAttribute($name)
+    {
+        $self = clone $this;
+        if (\array_key_exists($name, $self->attributes))
+        {
+            unset($self->attributes[$name]);
+        }
+
+        return $self;
+    }
+
+    /**
+     * 设置上传的文件.
+     *
+     * @param static                                         $object
+     * @param \Yurun\Util\YurunHttp\Http\Psr7\UploadedFile[] $files
+     *
+     * @return static
+     */
+    protected function setUploadedFiles(self $object, array $files)
+    {
+        $object->files = [];
+        foreach ($files as $name => $file)
+        {
+            if ($file instanceof UploadedFile)
+            {
+                $object->files[$name] = $file;
+            }
+            else
+            {
+                $object->files[$name] = new UploadedFile($file['name'], $file['type'], $file['tmp_name'], $file['size'], $file['error']);
+            }
+        }
+
+        return $object;
+    }
+}

+ 249 - 0
vendor/yurunsoft/yurun-http/src/YurunHttp/Http/Psr7/UploadedFile.php

@@ -0,0 +1,249 @@
+<?php
+
+namespace Yurun\Util\YurunHttp\Http\Psr7;
+
+use Psr\Http\Message\StreamInterface;
+use Psr\Http\Message\UploadedFileInterface;
+use Yurun\Util\YurunHttp\Stream\FileStream;
+
+class UploadedFile implements UploadedFileInterface
+{
+    /**
+     * 文件在客户端时的文件名.
+     *
+     * @var string
+     */
+    protected $fileName;
+
+    /**
+     * 文件mime类型.
+     *
+     * @var string
+     */
+    protected $mediaType;
+
+    /**
+     * 临时文件名.
+     *
+     * @var string
+     */
+    protected $tmpFileName;
+
+    /**
+     * 文件大小,单位:字节
+     *
+     * @var int
+     */
+    protected $size;
+
+    /**
+     * 错误码
+     *
+     * @var int
+     */
+    protected $error;
+
+    /**
+     * 文件流
+     *
+     * @var \Yurun\Util\YurunHttp\Stream\FileStream
+     */
+    protected $stream;
+
+    /**
+     * 文件是否被移动过.
+     *
+     * @var bool
+     */
+    protected $isMoved = false;
+
+    /**
+     * @param string $fileName
+     * @param string $mediaType
+     * @param string $tmpFileName
+     * @param int    $size
+     * @param int    $error
+     */
+    public function __construct($fileName, $mediaType, $tmpFileName, $size = null, $error = 0)
+    {
+        $this->fileName = $fileName;
+        $this->mediaType = $mediaType;
+        $this->tmpFileName = $tmpFileName;
+        if (null === $size)
+        {
+            $this->size = filesize($tmpFileName);
+        }
+        else
+        {
+            $this->size = $size;
+        }
+        $this->error = $error;
+    }
+
+    /**
+     * Retrieve a stream representing the uploaded file.
+     *
+     * This method MUST return a StreamInterface instance, representing the
+     * uploaded file. The purpose of this method is to allow utilizing native PHP
+     * stream functionality to manipulate the file upload, such as
+     * stream_copy_to_stream() (though the result will need to be decorated in a
+     * native PHP stream wrapper to work with such functions).
+     *
+     * If the moveTo() method has been called previously, this method MUST raise
+     * an exception.
+     *
+     * @return StreamInterface stream representation of the uploaded file
+     *
+     * @throws \RuntimeException in cases when no stream is available or can be
+     *                           created
+     */
+    public function getStream()
+    {
+        if (null === $this->stream)
+        {
+            $this->stream = new FileStream($this->tmpFileName);
+        }
+
+        return $this->stream;
+    }
+
+    /**
+     * Move the uploaded file to a new location.
+     *
+     * Use this method as an alternative to move_uploaded_file(). This method is
+     * guaranteed to work in both SAPI and non-SAPI environments.
+     * Implementations must determine which environment they are in, and use the
+     * appropriate method (move_uploaded_file(), rename(), or a stream
+     * operation) to perform the operation.
+     *
+     * $targetPath may be an absolute path, or a relative path. If it is a
+     * relative path, resolution should be the same as used by PHP's rename()
+     * function.
+     *
+     * The original file or stream MUST be removed on completion.
+     *
+     * If this method is called more than once, any subsequent calls MUST raise
+     * an exception.
+     *
+     * When used in an SAPI environment where $_FILES is populated, when writing
+     * files via moveTo(), is_uploaded_file() and move_uploaded_file() SHOULD be
+     * used to ensure permissions and upload status are verified correctly.
+     *
+     * If you wish to move to a stream, use getStream(), as SAPI operations
+     * cannot guarantee writing to stream destinations.
+     *
+     * @see http://php.net/is_uploaded_file
+     * @see http://php.net/move_uploaded_file
+     *
+     * @param string $targetPath path to which to move the uploaded file
+     *
+     * @return void
+     *
+     * @throws \InvalidArgumentException if the $path specified is invalid
+     * @throws \RuntimeException         on any error during the move operation, or on
+     *                                   the second or subsequent call to the method
+     */
+    public function moveTo($targetPath)
+    {
+        if (!\is_string($targetPath))
+        {
+            throw new \InvalidArgumentException('targetPath specified is invalid');
+        }
+        if ($this->isMoved)
+        {
+            throw new \RuntimeException('file can not be moved');
+        }
+        if (is_uploaded_file($this->tmpFileName))
+        {
+            $this->isMoved = move_uploaded_file($this->tmpFileName, $targetPath);
+        }
+        else
+        {
+            $this->isMoved = rename($this->tmpFileName, $targetPath);
+        }
+        if (!$this->isMoved)
+        {
+            throw new \RuntimeException(sprintf('file %s move to %s fail', $this->tmpFileName, $targetPath));
+        }
+    }
+
+    /**
+     * Retrieve the file size.
+     *
+     * Implementations SHOULD return the value stored in the "size" key of
+     * the file in the $_FILES array if available, as PHP calculates this based
+     * on the actual size transmitted.
+     *
+     * @return int|null the file size in bytes or null if unknown
+     */
+    public function getSize()
+    {
+        return $this->size;
+    }
+
+    /**
+     * Retrieve the error associated with the uploaded file.
+     *
+     * The return value MUST be one of PHP's UPLOAD_ERR_XXX constants.
+     *
+     * If the file was uploaded successfully, this method MUST return
+     * UPLOAD_ERR_OK.
+     *
+     * Implementations SHOULD return the value stored in the "error" key of
+     * the file in the $_FILES array.
+     *
+     * @see http://php.net/manual/en/features.file-upload.errors.php
+     *
+     * @return int one of PHP's UPLOAD_ERR_XXX constants
+     */
+    public function getError()
+    {
+        return $this->error;
+    }
+
+    /**
+     * Retrieve the filename sent by the client.
+     *
+     * Do not trust the value returned by this method. A client could send
+     * a malicious filename with the intention to corrupt or hack your
+     * application.
+     *
+     * Implementations SHOULD return the value stored in the "name" key of
+     * the file in the $_FILES array.
+     *
+     * @return string|null the filename sent by the client or null if none
+     *                     was provided
+     */
+    public function getClientFilename()
+    {
+        return $this->fileName;
+    }
+
+    /**
+     * Retrieve the media type sent by the client.
+     *
+     * Do not trust the value returned by this method. A client could send
+     * a malicious media type with the intention to corrupt or hack your
+     * application.
+     *
+     * Implementations SHOULD return the value stored in the "type" key of
+     * the file in the $_FILES array.
+     *
+     * @return string|null the media type sent by the client or null if none
+     *                     was provided
+     */
+    public function getClientMediaType()
+    {
+        return $this->mediaType;
+    }
+
+    /**
+     * 获取上传文件的临时文件路径.
+     *
+     * @return string
+     */
+    public function getTempFileName()
+    {
+        return $this->tmpFileName;
+    }
+}

+ 590 - 0
vendor/yurunsoft/yurun-http/src/YurunHttp/Http/Psr7/Uri.php

@@ -0,0 +1,590 @@
+<?php
+
+namespace Yurun\Util\YurunHttp\Http\Psr7;
+
+use Psr\Http\Message\UriInterface;
+
+class Uri implements UriInterface
+{
+    /**
+     * 协议,如:http.
+     *
+     * @var string
+     */
+    protected $scheme;
+
+    /**
+     * 主机名.
+     *
+     * @var string
+     */
+    protected $host;
+
+    /**
+     * 端口号.
+     *
+     * @var int|null
+     */
+    protected $port;
+
+    /**
+     * 用户信息
+     * 格式:用户名:密码
+     *
+     * @var string
+     */
+    protected $userInfo;
+
+    /**
+     * 路径.
+     *
+     * @var string
+     */
+    protected $path;
+
+    /**
+     * 查询参数,在?后的.
+     *
+     * @var string
+     */
+    protected $query;
+
+    /**
+     * 锚点,在#后的.
+     *
+     * @var string
+     */
+    protected $fragment;
+
+    /**
+     * 协议标准端口.
+     *
+     * @var array
+     */
+    protected static $schemePorts = [
+        'http'  => 80,
+        'https' => 443,
+        'ftp'   => 21,
+    ];
+
+    /**
+     * @param string $uri
+     */
+    public function __construct($uri = '')
+    {
+        $uriOption = parse_url($uri);
+        if (false === $uriOption)
+        {
+            throw new \InvalidArgumentException(sprintf('uri %s parse error', $uri));
+        }
+        $this->scheme = isset($uriOption['scheme']) ? $uriOption['scheme'] : '';
+        $this->host = isset($uriOption['host']) ? $uriOption['host'] : '';
+        $this->port = isset($uriOption['port']) ? $uriOption['port'] : null;
+        $this->userInfo = isset($uriOption['user']) ? $uriOption['user'] : '';
+        if (isset($uriOption['pass']))
+        {
+            $this->userInfo .= ':' . $uriOption['pass'];
+        }
+        $this->path = isset($uriOption['path']) ? $uriOption['path'] : '';
+        $this->query = isset($uriOption['query']) ? $uriOption['query'] : '';
+        $this->fragment = isset($uriOption['fragment']) ? $uriOption['fragment'] : '';
+    }
+
+    /**
+     * 生成Uri文本.
+     *
+     * @param string $host
+     * @param string $path
+     * @param string $query
+     * @param int    $port
+     * @param string $scheme
+     * @param string $fragment
+     * @param string $userInfo
+     *
+     * @return string
+     */
+    public static function makeUriString($host, $path, $query = '', $port = null, $scheme = 'http', $fragment = '', $userInfo = '')
+    {
+        $uri = '';
+        // 协议
+        if ('' !== $scheme)
+        {
+            $uri = $scheme . '://';
+        }
+        // 用户信息
+        if ('' !== $userInfo)
+        {
+            $uri .= $userInfo . '@';
+        }
+        // 主机+端口
+        $uri .= $host . (null === $port ? '' : (':' . $port));
+        // 路径
+        $uri .= '/' . ltrim($path, '/');
+        // 查询参数
+        $uri .= ('' === $query ? '' : ('?' . $query));
+        // 锚点
+        $uri .= ('' === $fragment ? '' : ('#' . $fragment));
+
+        return $uri;
+    }
+
+    /**
+     * 生成Uri对象
+     *
+     * @param string $host
+     * @param string $path
+     * @param string $query
+     * @param int    $port
+     * @param string $scheme
+     * @param string $fragment
+     * @param string $userInfo
+     *
+     * @return static
+     */
+    public static function makeUri($host, $path, $query = '', $port = 80, $scheme = 'http', $fragment = '', $userInfo = '')
+    {
+        return new static(static::makeUriString($host, $path, $query, $port, $scheme, $fragment, $userInfo));
+    }
+
+    /**
+     * 获取连接到服务器的端口.
+     *
+     * @param \Psr\Http\Message\UriInterface $uri
+     *
+     * @return int
+     */
+    public static function getServerPort(UriInterface $uri)
+    {
+        $port = $uri->getPort();
+        if (!$port)
+        {
+            $scheme = $uri->getScheme();
+            $port = isset(static::$schemePorts[$scheme]) ? static::$schemePorts[$scheme] : null;
+        }
+
+        return $port;
+    }
+
+    /**
+     * 获取域名
+     * 格式:host[:port].
+     *
+     * @param \Psr\Http\Message\UriInterface $uri
+     *
+     * @return string
+     */
+    public static function getDomain(UriInterface $uri)
+    {
+        $result = $uri->getHost();
+        if (null !== ($port = $uri->getPort()))
+        {
+            $result .= ':' . $port;
+        }
+
+        return $result;
+    }
+
+    /**
+     * Retrieve the scheme component of the URI.
+     *
+     * If no scheme is present, this method MUST return an empty string.
+     *
+     * The value returned MUST be normalized to lowercase, per RFC 3986
+     * Section 3.1.
+     *
+     * The trailing ":" character is not part of the scheme and MUST NOT be
+     * added.
+     *
+     * @see https://tools.ietf.org/html/rfc3986#section-3.1
+     *
+     * @return string the URI scheme
+     */
+    public function getScheme()
+    {
+        return $this->scheme;
+    }
+
+    /**
+     * Retrieve the authority component of the URI.
+     *
+     * If no authority information is present, this method MUST return an empty
+     * string.
+     *
+     * The authority syntax of the URI is:
+     *
+     * <pre>
+     * [user-info@]host[:port]
+     * </pre>
+     *
+     * If the port component is not set or is the standard port for the current
+     * scheme, it SHOULD NOT be included.
+     *
+     * @see https://tools.ietf.org/html/rfc3986#section-3.2
+     *
+     * @return string the URI authority, in "[user-info@]host[:port]" format
+     */
+    public function getAuthority()
+    {
+        $result = $this->host;
+        if ('' !== $this->userInfo)
+        {
+            $result = $this->userInfo . '@' . $result;
+        }
+        if (null !== $this->port)
+        {
+            $result .= ':' . $this->port;
+        }
+
+        return $result;
+    }
+
+    /**
+     * Retrieve the user information component of the URI.
+     *
+     * If no user information is present, this method MUST return an empty
+     * string.
+     *
+     * If a user is present in the URI, this will return that value;
+     * additionally, if the password is also present, it will be appended to the
+     * user value, with a colon (":") separating the values.
+     *
+     * The trailing "@" character is not part of the user information and MUST
+     * NOT be added.
+     *
+     * @return string the URI user information, in "username[:password]" format
+     */
+    public function getUserInfo()
+    {
+        return $this->userInfo;
+    }
+
+    /**
+     * Retrieve the host component of the URI.
+     *
+     * If no host is present, this method MUST return an empty string.
+     *
+     * The value returned MUST be normalized to lowercase, per RFC 3986
+     * Section 3.2.2.
+     *
+     * @see http://tools.ietf.org/html/rfc3986#section-3.2.2
+     *
+     * @return string the URI host
+     */
+    public function getHost()
+    {
+        return $this->host;
+    }
+
+    /**
+     * Retrieve the port component of the URI.
+     *
+     * If a port is present, and it is non-standard for the current scheme,
+     * this method MUST return it as an integer. If the port is the standard port
+     * used with the current scheme, this method SHOULD return null.
+     *
+     * If no port is present, and no scheme is present, this method MUST return
+     * a null value.
+     *
+     * If no port is present, but a scheme is present, this method MAY return
+     * the standard port for that scheme, but SHOULD return null.
+     *
+     * @return int|null the URI port
+     */
+    public function getPort()
+    {
+        return $this->port;
+    }
+
+    /**
+     * Retrieve the path component of the URI.
+     *
+     * The path can either be empty or absolute (starting with a slash) or
+     * rootless (not starting with a slash). Implementations MUST support all
+     * three syntaxes.
+     *
+     * Normally, the empty path "" and absolute path "/" are considered equal as
+     * defined in RFC 7230 Section 2.7.3. But this method MUST NOT automatically
+     * do this normalization because in contexts with a trimmed base path, e.g.
+     * the front controller, this difference becomes significant. It's the task
+     * of the user to handle both "" and "/".
+     *
+     * The value returned MUST be percent-encoded, but MUST NOT double-encode
+     * any characters. To determine what characters to encode, please refer to
+     * RFC 3986, Sections 2 and 3.3.
+     *
+     * As an example, if the value should include a slash ("/") not intended as
+     * delimiter between path segments, that value MUST be passed in encoded
+     * form (e.g., "%2F") to the instance.
+     *
+     * @see https://tools.ietf.org/html/rfc3986#section-2
+     * @see https://tools.ietf.org/html/rfc3986#section-3.3
+     *
+     * @return string the URI path
+     */
+    public function getPath()
+    {
+        return $this->path;
+    }
+
+    /**
+     * Retrieve the query string of the URI.
+     *
+     * If no query string is present, this method MUST return an empty string.
+     *
+     * The leading "?" character is not part of the query and MUST NOT be
+     * added.
+     *
+     * The value returned MUST be percent-encoded, but MUST NOT double-encode
+     * any characters. To determine what characters to encode, please refer to
+     * RFC 3986, Sections 2 and 3.4.
+     *
+     * As an example, if a value in a key/value pair of the query string should
+     * include an ampersand ("&") not intended as a delimiter between values,
+     * that value MUST be passed in encoded form (e.g., "%26") to the instance.
+     *
+     * @see https://tools.ietf.org/html/rfc3986#section-2
+     * @see https://tools.ietf.org/html/rfc3986#section-3.4
+     *
+     * @return string the URI query string
+     */
+    public function getQuery()
+    {
+        return $this->query;
+    }
+
+    /**
+     * Retrieve the fragment component of the URI.
+     *
+     * If no fragment is present, this method MUST return an empty string.
+     *
+     * The leading "#" character is not part of the fragment and MUST NOT be
+     * added.
+     *
+     * The value returned MUST be percent-encoded, but MUST NOT double-encode
+     * any characters. To determine what characters to encode, please refer to
+     * RFC 3986, Sections 2 and 3.5.
+     *
+     * @see https://tools.ietf.org/html/rfc3986#section-2
+     * @see https://tools.ietf.org/html/rfc3986#section-3.5
+     *
+     * @return string the URI fragment
+     */
+    public function getFragment()
+    {
+        return $this->fragment;
+    }
+
+    /**
+     * Return an instance with the specified scheme.
+     *
+     * This method MUST retain the state of the current instance, and return
+     * an instance that contains the specified scheme.
+     *
+     * Implementations MUST support the schemes "http" and "https" case
+     * insensitively, and MAY accommodate other schemes if required.
+     *
+     * An empty scheme is equivalent to removing the scheme.
+     *
+     * @param string $scheme the scheme to use with the new instance
+     *
+     * @return static a new instance with the specified scheme
+     *
+     * @throws \InvalidArgumentException for invalid or unsupported schemes
+     */
+    public function withScheme($scheme)
+    {
+        if (!\is_string($scheme))
+        {
+            throw new \InvalidArgumentException('invalid or unsupported schemes');
+        }
+        $self = clone $this;
+        $self->scheme = $scheme;
+
+        return $self;
+    }
+
+    /**
+     * Return an instance with the specified user information.
+     *
+     * This method MUST retain the state of the current instance, and return
+     * an instance that contains the specified user information.
+     *
+     * Password is optional, but the user information MUST include the
+     * user; an empty string for the user is equivalent to removing user
+     * information.
+     *
+     * @param string      $user     the user name to use for authority
+     * @param string|null $password the password associated with $user
+     *
+     * @return static a new instance with the specified user information
+     */
+    public function withUserInfo($user, $password = null)
+    {
+        $self = clone $this;
+        $self->userInfo = $user;
+        if (null !== $password)
+        {
+            $self->userInfo .= ':' . $password;
+        }
+
+        return $self;
+    }
+
+    /**
+     * Return an instance with the specified host.
+     *
+     * This method MUST retain the state of the current instance, and return
+     * an instance that contains the specified host.
+     *
+     * An empty host value is equivalent to removing the host.
+     *
+     * @param string $host the hostname to use with the new instance
+     *
+     * @return static a new instance with the specified host
+     *
+     * @throws \InvalidArgumentException for invalid hostnames
+     */
+    public function withHost($host)
+    {
+        $self = clone $this;
+        $self->host = $host;
+
+        return $self;
+    }
+
+    /**
+     * Return an instance with the specified port.
+     *
+     * This method MUST retain the state of the current instance, and return
+     * an instance that contains the specified port.
+     *
+     * Implementations MUST raise an exception for ports outside the
+     * established TCP and UDP port ranges.
+     *
+     * A null value provided for the port is equivalent to removing the port
+     * information.
+     *
+     * @param int|null $port the port to use with the new instance; a null value
+     *                       removes the port information
+     *
+     * @return static a new instance with the specified port
+     *
+     * @throws \InvalidArgumentException for invalid ports
+     */
+    public function withPort($port)
+    {
+        $self = clone $this;
+        $self->port = $port;
+
+        return $self;
+    }
+
+    /**
+     * Return an instance with the specified path.
+     *
+     * This method MUST retain the state of the current instance, and return
+     * an instance that contains the specified path.
+     *
+     * The path can either be empty or absolute (starting with a slash) or
+     * rootless (not starting with a slash). Implementations MUST support all
+     * three syntaxes.
+     *
+     * If the path is intended to be domain-relative rather than path relative then
+     * it must begin with a slash ("/"). Paths not starting with a slash ("/")
+     * are assumed to be relative to some base path known to the application or
+     * consumer.
+     *
+     * Users can provide both encoded and decoded path characters.
+     * Implementations ensure the correct encoding as outlined in getPath().
+     *
+     * @param string $path the path to use with the new instance
+     *
+     * @return static a new instance with the specified path
+     *
+     * @throws \InvalidArgumentException for invalid paths
+     */
+    public function withPath($path)
+    {
+        $self = clone $this;
+        $self->path = $path;
+
+        return $self;
+    }
+
+    /**
+     * Return an instance with the specified query string.
+     *
+     * This method MUST retain the state of the current instance, and return
+     * an instance that contains the specified query string.
+     *
+     * Users can provide both encoded and decoded query characters.
+     * Implementations ensure the correct encoding as outlined in getQuery().
+     *
+     * An empty query string value is equivalent to removing the query string.
+     *
+     * @param string $query the query string to use with the new instance
+     *
+     * @return static a new instance with the specified query string
+     *
+     * @throws \InvalidArgumentException for invalid query strings
+     */
+    public function withQuery($query)
+    {
+        $self = clone $this;
+        $self->query = $query;
+
+        return $self;
+    }
+
+    /**
+     * Return an instance with the specified URI fragment.
+     *
+     * This method MUST retain the state of the current instance, and return
+     * an instance that contains the specified URI fragment.
+     *
+     * Users can provide both encoded and decoded fragment characters.
+     * Implementations ensure the correct encoding as outlined in getFragment().
+     *
+     * An empty fragment value is equivalent to removing the fragment.
+     *
+     * @param string $fragment the fragment to use with the new instance
+     *
+     * @return static a new instance with the specified fragment
+     */
+    public function withFragment($fragment)
+    {
+        $self = clone $this;
+        $self->fragment = $fragment;
+
+        return $self;
+    }
+
+    /**
+     * Return the string representation as a URI reference.
+     *
+     * Depending on which components of the URI are present, the resulting
+     * string is either a full URI or relative reference according to RFC 3986,
+     * Section 4.1. The method concatenates the various components of the URI,
+     * using the appropriate delimiters:
+     *
+     * - If a scheme is present, it MUST be suffixed by ":".
+     * - If an authority is present, it MUST be prefixed by "//".
+     * - The path can be concatenated without delimiters. But there are two
+     *   cases where the path has to be adjusted to make the URI reference
+     *   valid as PHP does not allow to throw an exception in __toString():
+     *     - If the path is rootless and an authority is present, the path MUST
+     *       be prefixed by "/".
+     *     - If the path is starting with more than one "/" and no authority is
+     *       present, the starting slashes MUST be reduced to one.
+     * - If a query is present, it MUST be prefixed by "?".
+     * - If a fragment is present, it MUST be prefixed by "#".
+     *
+     * @see http://tools.ietf.org/html/rfc3986#section-4.1
+     *
+     * @return string
+     */
+    public function __toString()
+    {
+        return static::makeUriString($this->host, $this->path, $this->query, $this->port, $this->scheme, $this->fragment, $this->userInfo);
+    }
+}

+ 9 - 0
vendor/yurunsoft/yurun-http/src/YurunHttp/Http/Request.php

@@ -0,0 +1,9 @@
+<?php
+
+namespace Yurun\Util\YurunHttp\Http;
+
+use Yurun\Util\YurunHttp\Http\Psr7\ServerRequest as Psr7Request;
+
+class Request extends Psr7Request
+{
+}

+ 426 - 0
vendor/yurunsoft/yurun-http/src/YurunHttp/Http/Response.php

@@ -0,0 +1,426 @@
+<?php
+
+namespace Yurun\Util\YurunHttp\Http;
+
+use Yurun\Util\YurunHttp\Http\Psr7\Consts\StatusCode;
+use Yurun\Util\YurunHttp\Http\Psr7\Response as Psr7Response;
+
+class Response extends Psr7Response
+{
+    /**
+     * 是否请求成功
+     *
+     * @var bool
+     */
+    public $success;
+
+    /**
+     * cookie数据.
+     *
+     * @var array
+     */
+    protected $cookies;
+
+    /**
+     * cookie原始数据,包含expires、path、domain等.
+     *
+     * @var array
+     */
+    protected $cookiesOrigin;
+
+    /**
+     * 请求总耗时,单位:秒.
+     *
+     * @var float
+     */
+    protected $totalTime;
+
+    /**
+     * 错误信息.
+     *
+     * @var string
+     */
+    protected $error;
+
+    /**
+     * 错误码
+     *
+     * @var int
+     */
+    protected $errno;
+
+    /**
+     * Http2 streamId.
+     *
+     * @var int
+     */
+    protected $streamId;
+
+    /**
+     * Request.
+     *
+     * @var \Yurun\Util\YurunHttp\Http\Request
+     */
+    protected $request;
+
+    /**
+     * 保存到的文件名.
+     *
+     * @var string|null
+     */
+    protected $savedFileName;
+
+    /**
+     * Retrieve cookies.
+     *
+     * Retrieves cookies sent by the client to the server.
+     *
+     * The data MUST be compatible with the structure of the $_COOKIE
+     * superglobal.
+     *
+     * @return array
+     */
+    public function getCookieParams()
+    {
+        return $this->cookies;
+    }
+
+    /**
+     * 获取cookie值
+     *
+     * @param string $name
+     * @param mixed  $default
+     *
+     * @return mixed
+     */
+    public function getCookie($name, $default = null)
+    {
+        return isset($this->cookies[$name]) ? $this->cookies[$name] : $default;
+    }
+
+    /**
+     * 设置cookie原始参数,包含expires、path、domain等.
+     *
+     * @param array $cookiesOrigin
+     *
+     * @return static
+     */
+    public function withCookieOriginParams(array $cookiesOrigin)
+    {
+        $self = clone $this;
+        $self->cookiesOrigin = $cookiesOrigin;
+        $self->cookies = [];
+        foreach ($cookiesOrigin as $name => $value)
+        {
+            $self->cookies[$name] = $value['value'];
+        }
+
+        return $self;
+    }
+
+    /**
+     * 获取所有cookie原始参数,包含expires、path、domain等.
+     *
+     * @return array
+     */
+    public function getCookieOriginParams()
+    {
+        return $this->cookiesOrigin;
+    }
+
+    /**
+     * 获取cookie原始参数值,包含expires、path、domain等.
+     *
+     * @param string $name
+     * @param mixed  $default
+     *
+     * @return string
+     */
+    public function getCookieOrigin($name, $default = null)
+    {
+        return isset($this->cookiesOrigin[$name]) ? $this->cookiesOrigin[$name] : $default;
+    }
+
+    /**
+     * @param string|\Psr\Http\Message\StreamInterface $body
+     * @param int                                      $statusCode
+     * @param string|null                              $reasonPhrase
+     */
+    public function __construct($body = '', $statusCode = StatusCode::OK, $reasonPhrase = null)
+    {
+        parent::__construct($body, $statusCode, $reasonPhrase);
+        $this->success = $statusCode >= 100;
+    }
+
+    /**
+     * 获取返回的主体内容.
+     *
+     * @param string $fromEncoding 请求返回数据的编码,如果不为空则进行编码转换
+     * @param string $toEncoding   要转换到的编码,默认为UTF-8
+     *
+     * @return string
+     */
+    public function body($fromEncoding = null, $toEncoding = 'UTF-8')
+    {
+        if (null === $fromEncoding)
+        {
+            return (string) $this->getBody();
+        }
+        else
+        {
+            return mb_convert_encoding((string) $this->getBody(), $toEncoding, $fromEncoding);
+        }
+    }
+
+    /**
+     * 获取xml格式内容.
+     *
+     * @param bool   $assoc        为true时返回数组,为false时返回对象
+     * @param string $fromEncoding 请求返回数据的编码,如果不为空则进行编码转换
+     * @param string $toEncoding   要转换到的编码,默认为UTF-8
+     *
+     * @return mixed
+     */
+    public function xml($assoc = false, $fromEncoding = null, $toEncoding = 'UTF-8')
+    {
+        $xml = simplexml_load_string($this->body($fromEncoding, $toEncoding), 'SimpleXMLElement', \LIBXML_NOCDATA | \LIBXML_COMPACT);
+        if ($assoc)
+        {
+            $xml = (array) $xml;
+        }
+
+        return $xml;
+    }
+
+    /**
+     * 获取json格式内容.
+     *
+     * @param bool   $assoc        为true时返回数组,为false时返回对象
+     * @param string $fromEncoding 请求返回数据的编码,如果不为空则进行编码转换
+     * @param string $toEncoding   要转换到的编码,默认为UTF-8
+     *
+     * @return mixed
+     */
+    public function json($assoc = false, $fromEncoding = null, $toEncoding = 'UTF-8')
+    {
+        return json_decode($this->body($fromEncoding, $toEncoding), $assoc);
+    }
+
+    /**
+     * 获取jsonp格式内容.
+     *
+     * @param bool   $assoc        为true时返回数组,为false时返回对象
+     * @param string $fromEncoding 请求返回数据的编码,如果不为空则进行编码转换
+     * @param string $toEncoding   要转换到的编码,默认为UTF-8
+     *
+     * @return mixed
+     */
+    public function jsonp($assoc = false, $fromEncoding = null, $toEncoding = 'UTF-8')
+    {
+        $jsonp = trim($this->body($fromEncoding, $toEncoding));
+        if (isset($jsonp[0]) && '[' !== $jsonp[0] && '{' !== $jsonp[0])
+        {
+            $begin = strpos($jsonp, '(');
+            if (false !== $begin)
+            {
+                $end = strrpos($jsonp, ')');
+                if (false !== $end)
+                {
+                    $jsonp = substr($jsonp, $begin + 1, $end - $begin - 1);
+                }
+            }
+        }
+
+        return json_decode($jsonp, $assoc);
+    }
+
+    /**
+     * 获取http状态码
+     *
+     * @return int
+     */
+    public function httpCode()
+    {
+        return $this->getStatusCode();
+    }
+
+    /**
+     * 获取请求总耗时,单位:秒.
+     *
+     * @return float
+     */
+    public function totalTime()
+    {
+        return $this->totalTime;
+    }
+
+    /**
+     * 获取请求总耗时,单位:秒.
+     *
+     * @return float
+     */
+    public function getTotalTime()
+    {
+        return $this->totalTime;
+    }
+
+    /**
+     * 设置请求总耗时.
+     *
+     * @param float $totalTime
+     *
+     * @return static
+     */
+    public function withTotalTime($totalTime)
+    {
+        $self = clone $this;
+        $self->totalTime = $totalTime;
+
+        return $self;
+    }
+
+    /**
+     * 返回错误信息.
+     *
+     * @return string
+     */
+    public function error()
+    {
+        return $this->error;
+    }
+
+    /**
+     * 获取错误信息.
+     *
+     * @return string
+     */
+    public function getError()
+    {
+        return $this->error;
+    }
+
+    /**
+     * 设置错误信息.
+     *
+     * @param string $error
+     *
+     * @return static
+     */
+    public function withError($error)
+    {
+        $self = clone $this;
+        $self->error = $error;
+
+        return $self;
+    }
+
+    /**
+     * 返回错误代码
+     *
+     * @return int
+     */
+    public function errno()
+    {
+        return $this->errno;
+    }
+
+    /**
+     * 获取错误代码
+     *
+     * @return int
+     */
+    public function getErrno()
+    {
+        return $this->errno;
+    }
+
+    /**
+     * 设置错误代码
+     *
+     * @param int $errno
+     *
+     * @return static
+     */
+    public function withErrno($errno)
+    {
+        $self = clone $this;
+        $self->errno = $errno;
+
+        return $self;
+    }
+
+    /**
+     * 设置 Http2 streamId.
+     *
+     * @param int $streamId
+     *
+     * @return static
+     */
+    public function withStreamId($streamId)
+    {
+        $self = clone $this;
+        $self->streamId = $streamId;
+
+        return $self;
+    }
+
+    /**
+     * Get http2 streamId.
+     *
+     * @return int
+     */
+    public function getStreamId()
+    {
+        return $this->streamId;
+    }
+
+    /**
+     * 设置请求体.
+     *
+     * @param \Yurun\Util\YurunHttp\Http\Request $request
+     *
+     * @return static
+     */
+    public function withRequest($request)
+    {
+        $self = clone $this;
+        $self->request = $request;
+
+        return $self;
+    }
+
+    /**
+     * Get request.
+     *
+     * @return \Yurun\Util\YurunHttp\Http\Request
+     */
+    public function getRequest()
+    {
+        return $this->request;
+    }
+
+    /**
+     * 设置保存到的文件名.
+     *
+     * @param string|null $savedFileName
+     *
+     * @return static
+     */
+    public function withSavedFileName($savedFileName)
+    {
+        $self = clone $this;
+        $self->savedFileName = $savedFileName;
+
+        return $self;
+    }
+
+    /**
+     * 获取保存到的文件名.
+     *
+     * @return string|null
+     */
+    public function getSavedFileName()
+    {
+        return $this->savedFileName;
+    }
+}

+ 128 - 0
vendor/yurunsoft/yurun-http/src/YurunHttp/Http2/IHttp2Client.php

@@ -0,0 +1,128 @@
+<?php
+
+namespace Yurun\Util\YurunHttp\Http2;
+
+interface IHttp2Client
+{
+    /**
+     * @param string $host
+     * @param int    $port
+     * @param bool   $ssl
+     * @param mixed  $handler
+     */
+    public function __construct($host, $port, $ssl, $handler = null);
+
+    /**
+     * 连接.
+     *
+     * @return bool
+     */
+    public function connect();
+
+    /**
+     * 获取 Http Handler.
+     *
+     * @return \Yurun\Util\YurunHttp\Handler\IHandler
+     */
+    public function getHttpHandler();
+
+    /**
+     * 关闭连接.
+     *
+     * @return void
+     */
+    public function close();
+
+    /**
+     * 发送数据
+     * 成功返回streamId,失败返回false.
+     *
+     * @param \Yurun\Util\YurunHttp\Http\Request $request
+     * @param bool                               $pipeline         默认send方法在发送请求之后,会结束当前的Http2 Stream,启用PIPELINE后,底层会保持stream流,可以多次调用write方法,向服务器发送数据帧,请参考write方法
+     * @param bool                               $dropRecvResponse 丢弃接收到的响应数据
+     *
+     * @return int|bool
+     */
+    public function send($request, $pipeline = false, $dropRecvResponse = false);
+
+    /**
+     * 向一个流写入数据帧.
+     *
+     * @param int    $streamId
+     * @param string $data
+     * @param bool   $end      是否关闭流
+     *
+     * @return bool
+     */
+    public function write($streamId, $data, $end = false);
+
+    /**
+     * 关闭一个流
+     *
+     * @param int $streamId
+     *
+     * @return bool
+     */
+    public function end($streamId);
+
+    /**
+     * 接收数据.
+     *
+     * @param int|null   $streamId 默认不传为 -1 时则监听服务端推送
+     * @param float|null $timeout  超时时间,单位:秒。默认为 null 不限制
+     *
+     * @return \Yurun\Util\YurunHttp\Http\Response|bool
+     */
+    public function recv($streamId = -1, $timeout = null);
+
+    /**
+     * 是否已连接.
+     *
+     * @return bool
+     */
+    public function isConnected();
+
+    /**
+     * Get 主机名.
+     *
+     * @return string
+     */
+    public function getHost();
+
+    /**
+     * Get 端口.
+     *
+     * @return int
+     */
+    public function getPort();
+
+    /**
+     * Get 是否使用 ssl.
+     *
+     * @return bool
+     */
+    public function isSSL();
+
+    /**
+     * 获取正在接收的流数量.
+     *
+     * @return int
+     */
+    public function getRecvingCount();
+
+    /**
+     * 设置超时时间,单位:秒.
+     *
+     * @param float|null $timeout
+     *
+     * @return void
+     */
+    public function setTimeout($timeout);
+
+    /**
+     * 获取超时时间,单位:秒.
+     *
+     * @return float|null
+     */
+    public function getTimeout();
+}

+ 412 - 0
vendor/yurunsoft/yurun-http/src/YurunHttp/Http2/SwooleClient.php

@@ -0,0 +1,412 @@
+<?php
+
+namespace Yurun\Util\YurunHttp\Http2;
+
+use Swoole\Coroutine;
+use Swoole\Coroutine\Channel;
+use Yurun\Util\YurunHttp\Attributes;
+use Yurun\Util\YurunHttp\Http\Psr7\Uri;
+
+class SwooleClient implements IHttp2Client
+{
+    /**
+     * 主机名.
+     *
+     * @var string
+     */
+    private $host;
+
+    /**
+     * 端口.
+     *
+     * @var int
+     */
+    private $port;
+
+    /**
+     * 是否使用 ssl.
+     *
+     * @var bool
+     */
+    private $ssl;
+
+    /**
+     * Swoole 协程客户端对象
+     *
+     * @var \Yurun\Util\YurunHttp\Handler\Swoole
+     */
+    private $handler;
+
+    /**
+     * Swoole http2 客户端.
+     *
+     * @var \Swoole\Coroutine\Http2\Client|null
+     */
+    private $http2Client;
+
+    /**
+     * 接收的频道集合.
+     *
+     * @var \Swoole\Coroutine\Channel[]
+     */
+    private $recvChannels = [];
+
+    /**
+     * 服务端推送数据队列长度.
+     *
+     * @var int
+     */
+    private $serverPushQueueLength = 16;
+
+    /**
+     * 请求集合.
+     *
+     * @var \Yurun\Util\YurunHttp\Http\Request[]
+     */
+    private $requestMap = [];
+
+    /**
+     * 超时时间,单位:秒.
+     *
+     * @var float
+     */
+    private $timeout;
+
+    /**
+     * 接收协程ID.
+     *
+     * @var int|bool
+     */
+    private $recvCo;
+
+    /**
+     * @param string                               $host
+     * @param int                                  $port
+     * @param bool                                 $ssl
+     * @param \Yurun\Util\YurunHttp\Handler\Swoole $handler
+     */
+    public function __construct($host, $port, $ssl, $handler = null)
+    {
+        $this->host = $host;
+        $this->port = $port;
+        $this->ssl = $ssl;
+        if ($handler)
+        {
+            $this->handler = $handler;
+        }
+        else
+        {
+            $this->handler = new \Yurun\Util\YurunHttp\Handler\Swoole();
+        }
+    }
+
+    /**
+     * 连接.
+     *
+     * @return bool
+     */
+    public function connect()
+    {
+        $url = ($this->ssl ? 'https://' : 'http://') . $this->host . ':' . $this->port;
+        $client = $this->handler->getHttp2ConnectionManager()->getConnection($url);
+        if ($client)
+        {
+            $this->http2Client = $client;
+            if ($this->timeout)
+            {
+                $client->set([
+                    'timeout'   => $this->timeout,
+                ]);
+            }
+
+            return true;
+        }
+        else
+        {
+            return false;
+        }
+    }
+
+    /**
+     * 获取 Http Handler.
+     *
+     * @return \Yurun\Util\YurunHttp\Handler\IHandler
+     */
+    public function getHttpHandler()
+    {
+        return $this->handler;
+    }
+
+    /**
+     * 关闭连接.
+     *
+     * @return void
+     */
+    public function close()
+    {
+        $this->http2Client = null;
+        $url = ($this->ssl ? 'https://' : 'http://') . $this->host . ':' . $this->port;
+        $this->handler->getHttp2ConnectionManager()->closeConnection($url);
+        $recvChannels = &$this->recvChannels;
+        foreach ($recvChannels as $channel)
+        {
+            $channel->close();
+        }
+        $recvChannels = [];
+    }
+
+    /**
+     * 发送数据
+     * 成功返回streamId,失败返回false.
+     *
+     * @param \Yurun\Util\YurunHttp\Http\Request $request
+     * @param bool                               $pipeline         默认send方法在发送请求之后,会结束当前的Http2 Stream,启用PIPELINE后,底层会保持stream流,可以多次调用write方法,向服务器发送数据帧,请参考write方法
+     * @param bool                               $dropRecvResponse 丢弃接收到的响应数据
+     *
+     * @return int|bool
+     */
+    public function send($request, $pipeline = false, $dropRecvResponse = false)
+    {
+        if ('2.0' !== $request->getProtocolVersion())
+        {
+            $request = $request->withProtocolVersion('2.0');
+        }
+        $uri = $request->getUri();
+        if ($this->host != $uri->getHost() || $this->port != Uri::getServerPort($uri) || $this->ssl != ('https' === $uri->getScheme() || 'wss' === $uri->getScheme()))
+        {
+            throw new \RuntimeException(sprintf('Current http2 connection instance just support %s://%s:%s, does not support %s', $this->ssl ? 'https' : 'http', $this->host, $this->port, $uri->__toString()));
+        }
+        $http2Client = $this->http2Client;
+        $request = $request->withAttribute(Attributes::HTTP2_PIPELINE, $pipeline);
+        $this->handler->buildRequest($request, $http2Client, $http2Request);
+        $streamId = $http2Client->send($http2Request);
+        if (!$streamId)
+        {
+            $this->close();
+        }
+        if (!$dropRecvResponse)
+        {
+            $this->recvChannels[$streamId] = new Channel(1);
+            $this->requestMap[$streamId] = $request;
+        }
+
+        return $streamId;
+    }
+
+    /**
+     * 向一个流写入数据帧.
+     *
+     * @param int    $streamId
+     * @param string $data
+     * @param bool   $end      是否关闭流
+     *
+     * @return bool
+     */
+    public function write($streamId, $data, $end = false)
+    {
+        return $this->http2Client->write($streamId, $data, $end);
+    }
+
+    /**
+     * 关闭一个流
+     *
+     * @param int $streamId
+     *
+     * @return bool
+     */
+    public function end($streamId)
+    {
+        return $this->http2Client->write($streamId, '', true);
+    }
+
+    /**
+     * 接收数据.
+     *
+     * @param int|null   $streamId 默认不传为 -1 时则监听服务端推送
+     * @param float|null $timeout  超时时间,单位:秒。默认为 null 不限制
+     *
+     * @return \Yurun\Util\YurunHttp\Http\Response|bool
+     */
+    public function recv($streamId = -1, $timeout = null)
+    {
+        $recvCo = $this->recvCo;
+        if (!$recvCo || (true !== $recvCo && !Coroutine::exists($recvCo)))
+        {
+            $this->startRecvCo();
+        }
+        $recvChannels = &$this->recvChannels;
+        if (isset($recvChannels[$streamId]))
+        {
+            $channel = $recvChannels[$streamId];
+        }
+        else
+        {
+            $recvChannels[$streamId] = $channel = new Channel(-1 === $streamId ? $this->serverPushQueueLength : 1);
+        }
+        $swooleResponse = $channel->pop($timeout);
+        if (-1 !== $streamId)
+        {
+            unset($recvChannels[$streamId]);
+            $channel->close();
+        }
+        $requestMap = &$this->requestMap;
+        if (isset($requestMap[$streamId]))
+        {
+            $request = $requestMap[$streamId];
+            unset($requestMap[$streamId]);
+        }
+        else
+        {
+            $request = null;
+        }
+        $response = $this->handler->buildHttp2Response($request, $this->http2Client, $swooleResponse);
+
+        return $response;
+    }
+
+    /**
+     * 是否已连接.
+     *
+     * @return bool
+     */
+    public function isConnected()
+    {
+        return null !== $this->http2Client;
+    }
+
+    /**
+     * 开始接收协程
+     * 成功返回协程ID.
+     *
+     * @return int|bool
+     */
+    private function startRecvCo()
+    {
+        if (!$this->isConnected())
+        {
+            return false;
+        }
+        $recvCo = &$this->recvCo;
+        $recvCo = true;
+
+        return $recvCo = Coroutine::create(function () {
+            $http2Client = &$this->http2Client;
+            $recvChannels = &$this->recvChannels;
+            while ($this->isConnected())
+            {
+                if ($this->timeout > 0)
+                {
+                    $swooleResponse = $http2Client->recv($this->timeout);
+                }
+                else
+                {
+                    $swooleResponse = $http2Client->recv();
+                }
+                if (!$swooleResponse)
+                {
+                    $this->close();
+
+                    return;
+                }
+                $streamId = $swooleResponse->streamId;
+                if (isset($recvChannels[$streamId]) || (0 === ($streamId & 1) && isset($recvChannels[$streamId = -1])))
+                {
+                    $recvChannels[$streamId]->push($swooleResponse);
+                }
+            }
+        });
+    }
+
+    /**
+     * Get 主机名.
+     *
+     * @return string
+     */
+    public function getHost()
+    {
+        return $this->host;
+    }
+
+    /**
+     * Get 端口.
+     *
+     * @return int
+     */
+    public function getPort()
+    {
+        return $this->port;
+    }
+
+    /**
+     * Get 是否使用 ssl.
+     *
+     * @return bool
+     */
+    public function isSSL()
+    {
+        return $this->ssl;
+    }
+
+    /**
+     * 获取正在接收的流数量.
+     *
+     * @return int
+     */
+    public function getRecvingCount()
+    {
+        return \count($this->recvChannels);
+    }
+
+    /**
+     * Get 服务端推送数据队列长度.
+     *
+     * @return int
+     */
+    public function getServerPushQueueLength()
+    {
+        return $this->serverPushQueueLength;
+    }
+
+    /**
+     * Set 服务端推送数据队列长度.
+     *
+     * @param int $serverPushQueueLength 服务端推送数据队列长度
+     *
+     * @return self
+     */
+    public function setServerPushQueueLength($serverPushQueueLength)
+    {
+        $this->serverPushQueueLength = $serverPushQueueLength;
+
+        return $this;
+    }
+
+    /**
+     * 设置超时时间,单位:秒.
+     *
+     * @param float|null $timeout
+     *
+     * @return void
+     */
+    public function setTimeout($timeout)
+    {
+        $this->timeout = $timeout;
+        $http2Client = $this->http2Client;
+        if ($http2Client)
+        {
+            $http2Client->set([
+                'timeout'   => $timeout,
+            ]);
+        }
+    }
+
+    /**
+     * 获取超时时间,单位:秒.
+     *
+     * @return float|null
+     */
+    public function getTimeout()
+    {
+        return $this->timeout;
+    }
+}

+ 33 - 0
vendor/yurunsoft/yurun-http/src/YurunHttp/Pool/BaseConnectionPool.php

@@ -0,0 +1,33 @@
+<?php
+
+namespace Yurun\Util\YurunHttp\Pool;
+
+use Yurun\Util\YurunHttp\Pool\Contract\IConnectionPool;
+
+abstract class BaseConnectionPool implements IConnectionPool
+{
+    /**
+     * 连接池配置.
+     *
+     * @var \Yurun\Util\YurunHttp\Pool\Config\PoolConfig
+     */
+    protected $config;
+
+    /**
+     * @param \Yurun\Util\YurunHttp\Pool\Config\PoolConfig $config
+     */
+    public function __construct($config)
+    {
+        $this->config = $config;
+    }
+
+    /**
+     * 获取连接池配置.
+     *
+     * @return \Yurun\Util\YurunHttp\Pool\Config\PoolConfig
+     */
+    public function getConfig()
+    {
+        return $this->config;
+    }
+}

+ 93 - 0
vendor/yurunsoft/yurun-http/src/YurunHttp/Pool/Config/PoolConfig.php

@@ -0,0 +1,93 @@
+<?php
+
+namespace Yurun\Util\YurunHttp\Pool\Config;
+
+class PoolConfig
+{
+    /**
+     * 地址
+     *
+     * @var string
+     */
+    protected $url;
+
+    /**
+     * 最大连接数量.
+     *
+     * @var int
+     */
+    protected $maxConnections;
+
+    /**
+     * 等待超时时间.
+     *
+     * @var float
+     */
+    protected $waitTimeout;
+
+    /**
+     * @param string $url
+     * @param int    $maxConnections
+     * @param float  $waitTimeout
+     */
+    public function __construct($url, $maxConnections, $waitTimeout)
+    {
+        $this->url = $url;
+        $this->maxConnections = $maxConnections;
+        $this->waitTimeout = $waitTimeout;
+    }
+
+    /**
+     * 获取最大连接数量.
+     *
+     * @return int
+     */
+    public function getMaxConnections()
+    {
+        return $this->maxConnections;
+    }
+
+    /**
+     * 设置最大连接数量.
+     *
+     * @param int $maxConnections
+     *
+     * @return void
+     */
+    public function setMaxConnections($maxConnections)
+    {
+        $this->maxConnections = $maxConnections;
+    }
+
+    /**
+     * 获取等待超时时间.
+     *
+     * @return float
+     */
+    public function getWaitTimeout()
+    {
+        return $this->waitTimeout;
+    }
+
+    /**
+     * 设置等待超时时间.
+     *
+     * @param float $waitTimeout
+     *
+     * @return void
+     */
+    public function setWaitTimeout($waitTimeout)
+    {
+        $this->waitTimeout = $waitTimeout;
+    }
+
+    /**
+     * Get 地址
+     *
+     * @return string
+     */
+    public function getUrl()
+    {
+        return $this->url;
+    }
+}

+ 64 - 0
vendor/yurunsoft/yurun-http/src/YurunHttp/Pool/Contract/IConnectionPool.php

@@ -0,0 +1,64 @@
+<?php
+
+namespace Yurun\Util\YurunHttp\Pool\Contract;
+
+interface IConnectionPool
+{
+    /**
+     * 关闭连接池和连接池中的连接.
+     *
+     * @return void
+     */
+    public function close();
+
+    /**
+     * 创建一个连接,但不受连接池管理.
+     *
+     * @return mixed
+     */
+    public function createConnection();
+
+    /**
+     * 获取连接.
+     *
+     * @return mixed
+     */
+    public function getConnection();
+
+    /**
+     * 释放连接占用.
+     *
+     * @param mixed $connection
+     *
+     * @return void
+     */
+    public function release($connection);
+
+    /**
+     * 获取当前池子中连接总数.
+     *
+     * @return int
+     */
+    public function getCount();
+
+    /**
+     * 获取当前池子中空闲连接总数.
+     *
+     * @return int
+     */
+    public function getFree();
+
+    /**
+     * 获取当前池子中正在使用的连接总数.
+     *
+     * @return int
+     */
+    public function getUsed();
+
+    /**
+     * 获取连接池配置.
+     *
+     * @return \Yurun\Util\YurunHttp\Pool\Config\PoolConfig
+     */
+    public function getConfig();
+}

+ 59 - 0
vendor/yurunsoft/yurun-http/src/YurunHttp/Pool/Traits/TConnectionPoolConfigs.php

@@ -0,0 +1,59 @@
+<?php
+
+namespace Yurun\Util\YurunHttp\Pool\Traits;
+
+use Yurun\Util\YurunHttp\Pool\Config\PoolConfig;
+
+trait TConnectionPoolConfigs
+{
+    /**
+     * 连接池配置集合.
+     *
+     * @var PoolConfig[]
+     */
+    private $connectionPoolConfigs = [];
+
+    /**
+     * 设置连接池配置.
+     *
+     * @param string $url
+     * @param int    $maxConnections
+     * @param int    $waitTimeout
+     *
+     * @return PoolConfig
+     */
+    public function setConfig($url, $maxConnections = 0, $waitTimeout = 30)
+    {
+        if (isset($this->connectionPoolConfigs[$url]))
+        {
+            $config = $this->connectionPoolConfigs[$url];
+            $config->setMaxConnections($maxConnections);
+            $config->setWaitTimeout($waitTimeout);
+
+            return $config;
+        }
+        else
+        {
+            return $this->connectionPoolConfigs[$url] = new PoolConfig($url, $maxConnections, $waitTimeout);
+        }
+    }
+
+    /**
+     * 获取连接池配置.
+     *
+     * @param string $url
+     *
+     * @return PoolConfig|null
+     */
+    public function getConfig($url)
+    {
+        if (isset($this->connectionPoolConfigs[$url]))
+        {
+            return $this->connectionPoolConfigs[$url];
+        }
+        else
+        {
+            return null;
+        }
+    }
+}

+ 94 - 0
vendor/yurunsoft/yurun-http/src/YurunHttp/Random.php

@@ -0,0 +1,94 @@
+<?php
+
+namespace Yurun\Util\YurunHttp;
+
+abstract class Random
+{
+    /**
+     * 随机整数.
+     *
+     * @param int $min
+     * @param int $max
+     *
+     * @return int
+     */
+    public static function int($min = \PHP_INT_MIN, $max = \PHP_INT_MAX)
+    {
+        return mt_rand($min, $max);
+    }
+
+    /**
+     * 随机生成小数.
+     *
+     * @param float $min
+     * @param float $max
+     * @param int   $precision 最大小数位数
+     *
+     * @return float
+     */
+    public static function number($min = \PHP_INT_MIN, $max = \PHP_INT_MAX, $precision = 2)
+    {
+        return round($min + mt_rand() / mt_getrandmax() * ($max - $min), $precision);
+    }
+
+    /**
+     * 随机生成文本.
+     *
+     * @param string $chars
+     * @param int    $min
+     * @param int    $max
+     *
+     * @return string
+     */
+    public static function text($chars, $min, $max)
+    {
+        $length = mt_rand($min, $max);
+        $charLength = mb_strlen($chars);
+        $result = '';
+        for ($i = 0; $i < $length; ++$i)
+        {
+            $result .= mb_substr($chars, mt_rand(1, $charLength) - 1, 1);
+        }
+
+        return $result;
+    }
+
+    /**
+     * 随机生成字母.
+     *
+     * @param int $min
+     * @param int $max
+     *
+     * @return string
+     */
+    public static function letter($min, $max)
+    {
+        return static::text('abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ', $min, $max);
+    }
+
+    /**
+     * 随机生成数字.
+     *
+     * @param int $min
+     * @param int $max
+     *
+     * @return string
+     */
+    public static function digital($min, $max)
+    {
+        return static::text('0123456789', $min, $max);
+    }
+
+    /**
+     * 随机生成字母和数字.
+     *
+     * @param int $min
+     * @param int $max
+     *
+     * @return string
+     */
+    public static function letterAndNumber($min, $max)
+    {
+        return static::text('abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789', $min, $max);
+    }
+}

+ 362 - 0
vendor/yurunsoft/yurun-http/src/YurunHttp/Stream/FileStream.php

@@ -0,0 +1,362 @@
+<?php
+
+namespace Yurun\Util\YurunHttp\Stream;
+
+use Psr\Http\Message\StreamInterface;
+use Psr\Http\Message\UriInterface;
+use Yurun\Util\YurunHttp\Http\Psr7\Uri;
+
+class FileStream implements StreamInterface
+{
+    /**
+     * 文件Uri.
+     *
+     * @var UriInterface
+     */
+    protected $uri;
+
+    /**
+     * 流对象
+     *
+     * @var resource|null
+     */
+    protected $stream;
+
+    /**
+     * 流访问类型.
+     *
+     * @var string
+     */
+    protected $mode;
+
+    /**
+     * @param string|UriInterface $uri
+     * @param string              $mode
+     */
+    public function __construct($uri, $mode = StreamMode::READ_WRITE)
+    {
+        if (\is_string($uri))
+        {
+            $this->uri = $uri = new Uri($uri);
+        }
+        elseif ($uri instanceof UriInterface)
+        {
+            $this->uri = $uri;
+        }
+        else
+        {
+            $uri = $this->uri;
+        }
+        $this->mode = $mode;
+        $stream = fopen($uri, $mode);
+        if (false === $stream)
+        {
+            throw new \RuntimeException(sprintf('Open stream %s error', (string) $uri));
+        }
+        $this->stream = $stream;
+    }
+
+    public function __destruct()
+    {
+        if ($this->stream)
+        {
+            $this->close();
+        }
+    }
+
+    /**
+     * Reads all data from the stream into a string, from the beginning to end.
+     *
+     * This method MUST attempt to seek to the beginning of the stream before
+     * reading data and read the stream until the end is reached.
+     *
+     * Warning: This could attempt to load a large amount of data into memory.
+     *
+     * This method MUST NOT raise an exception in order to conform with PHP's
+     * string casting operations.
+     *
+     * @see http://php.net/manual/en/language.oop5.magic.php#object.tostring
+     *
+     * @return string
+     */
+    public function __toString()
+    {
+        try
+        {
+            $this->rewind();
+
+            return stream_get_contents($this->stream);
+        }
+        catch (\Throwable $ex)
+        {
+            return '';
+        }
+    }
+
+    /**
+     * Closes the stream and any underlying resources.
+     *
+     * @return void
+     */
+    public function close()
+    {
+        fclose($this->stream);
+        $this->stream = null;
+    }
+
+    /**
+     * Separates any underlying resources from the stream.
+     *
+     * After the stream has been detached, the stream is in an unusable state.
+     *
+     * @return resource|null Underlying PHP stream, if any
+     */
+    public function detach()
+    {
+        $stream = $this->stream;
+        $this->stream = null;
+
+        return $stream;
+    }
+
+    /**
+     * Get the size of the stream if known.
+     *
+     * @return int|null returns the size in bytes if known, or null if unknown
+     */
+    public function getSize()
+    {
+        $stat = fstat($this->stream);
+        if (false === $stat)
+        {
+            throw new \RuntimeException('get stream size error');
+        }
+
+        return $stat['size'];
+    }
+
+    /**
+     * Returns the current position of the file read/write pointer.
+     *
+     * @return int Position of the file pointer
+     *
+     * @throws \RuntimeException on error
+     */
+    public function tell()
+    {
+        $result = ftell($this->stream);
+        if (false === $result)
+        {
+            throw new \RuntimeException('stream tell error');
+        }
+
+        return $result;
+    }
+
+    /**
+     * Returns true if the stream is at the end of the stream.
+     *
+     * @return bool
+     */
+    public function eof()
+    {
+        return feof($this->stream);
+    }
+
+    /**
+     * Returns whether or not the stream is seekable.
+     *
+     * @return bool
+     */
+    public function isSeekable()
+    {
+        return (bool) $this->getMetadata('seekable');
+    }
+
+    /**
+     * Seek to a position in the stream.
+     *
+     * @see http://www.php.net/manual/en/function.fseek.php
+     *
+     * @param int $offset Stream offset
+     * @param int $whence Specifies how the cursor position will be calculated
+     *                    based on the seek offset. Valid values are identical to the built-in
+     *                    PHP $whence values for `fseek()`.  SEEK_SET: Set position equal to
+     *                    offset bytes SEEK_CUR: Set position to current location plus offset
+     *                    SEEK_END: Set position to end-of-stream plus offset.
+     *
+     * @return void
+     *
+     * @throws \RuntimeException on failure
+     */
+    public function seek($offset, $whence = \SEEK_SET)
+    {
+        if (-1 === fseek($this->stream, $offset, $whence))
+        {
+            throw new \RuntimeException('seek stream error');
+        }
+    }
+
+    /**
+     * Seek to the beginning of the stream.
+     *
+     * If the stream is not seekable, this method will raise an exception;
+     * otherwise, it will perform a seek(0).
+     *
+     * @see seek()
+     * @see http://www.php.net/manual/en/function.fseek.php
+     *
+     * @return void
+     *
+     * @throws \RuntimeException on failure
+     */
+    public function rewind()
+    {
+        if (!rewind($this->stream))
+        {
+            throw new \RuntimeException('rewind stream failed');
+        }
+    }
+
+    /**
+     * Returns whether or not the stream is writable.
+     *
+     * @return bool
+     */
+    public function isWritable()
+    {
+        return \in_array($this->mode, [
+            StreamMode::WRITE_CLEAN,
+            StreamMode::WRITE_END,
+            StreamMode::CREATE_READ_WRITE,
+            StreamMode::CREATE_WRITE,
+            StreamMode::READ_WRITE,
+            StreamMode::READ_WRITE_CLEAN,
+            StreamMode::READ_WRITE_END,
+        ]);
+    }
+
+    /**
+     * Write data to the stream.
+     *
+     * @param string $string the string that is to be written
+     *
+     * @return int returns the number of bytes written to the stream
+     *
+     * @throws \RuntimeException on failure
+     */
+    public function write($string)
+    {
+        $result = fwrite($this->stream, $string);
+        if (false === $result)
+        {
+            throw new \RuntimeException('write stream failed');
+        }
+
+        return $result;
+    }
+
+    /**
+     * Returns whether or not the stream is readable.
+     *
+     * @return bool
+     */
+    public function isReadable()
+    {
+        return \in_array($this->mode, [
+            StreamMode::READ_WRITE,
+            StreamMode::READ_WRITE_CLEAN,
+            StreamMode::READ_WRITE_END,
+            StreamMode::READONLY,
+            StreamMode::CREATE_READ_WRITE,
+        ]);
+    }
+
+    /**
+     * Read data from the stream.
+     *
+     * @param int $length Read up to $length bytes from the object and return
+     *                    them. Fewer than $length bytes may be returned if underlying stream
+     *                    call returns fewer bytes.
+     *
+     * @return string returns the data read from the stream, or an empty string
+     *                if no bytes are available
+     *
+     * @throws \RuntimeException if an error occurs
+     */
+    public function read($length)
+    {
+        $result = fread($this->stream, $length);
+        if (false === $result)
+        {
+            throw new \RuntimeException('read stream error');
+        }
+
+        return $result;
+    }
+
+    /**
+     * Returns the remaining contents in a string.
+     *
+     * @return string
+     *
+     * @throws \RuntimeException if unable to read or an error occurs while
+     *                           reading
+     */
+    public function getContents()
+    {
+        $result = stream_get_contents($this->stream);
+        if (false === $result)
+        {
+            throw new \RuntimeException('stream getContents error');
+        }
+
+        return $result;
+    }
+
+    /**
+     * Get stream metadata as an associative array or retrieve a specific key.
+     *
+     * The keys returned are identical to the keys returned from PHP's
+     * stream_get_meta_data() function.
+     *
+     * @see http://php.net/manual/en/function.stream-get-meta-data.php
+     *
+     * @param string $key specific metadata to retrieve
+     *
+     * @return array|mixed|null Returns an associative array if no key is
+     *                          provided. Returns a specific key value if a key is provided and the
+     *                          value is found, or null if the key is not found.
+     */
+    public function getMetadata($key = null)
+    {
+        $result = stream_get_meta_data($this->stream);
+        /* @phpstan-ignore-next-line */
+        if (!$result)
+        {
+            throw new \RuntimeException('stream getMetadata error');
+        }
+        if (null === $key)
+        {
+            return $result;
+        }
+        elseif (isset($result[$key]))
+        {
+            return $result[$key];
+        }
+        else
+        {
+            return null;
+        }
+    }
+
+    /**
+     * Get Uri.
+     *
+     * @return UriInterface
+     */
+    public function getUri()
+    {
+        return $this->uri;
+    }
+}

+ 281 - 0
vendor/yurunsoft/yurun-http/src/YurunHttp/Stream/MemoryStream.php

@@ -0,0 +1,281 @@
+<?php
+
+namespace Yurun\Util\YurunHttp\Stream;
+
+use Psr\Http\Message\StreamInterface;
+
+class MemoryStream implements StreamInterface
+{
+    /**
+     * 内容.
+     *
+     * @var string
+     */
+    protected $content;
+
+    /**
+     * 大小.
+     *
+     * @var int
+     */
+    protected $size;
+
+    /**
+     * 当前位置.
+     *
+     * @var int
+     */
+    protected $position = 0;
+
+    /**
+     * @param string $content
+     */
+    public function __construct($content = '')
+    {
+        $this->content = $content;
+        $this->size = \strlen($content);
+    }
+
+    /**
+     * Reads all data from the stream into a string, from the beginning to end.
+     *
+     * This method MUST attempt to seek to the beginning of the stream before
+     * reading data and read the stream until the end is reached.
+     *
+     * Warning: This could attempt to load a large amount of data into memory.
+     *
+     * This method MUST NOT raise an exception in order to conform with PHP's
+     * string casting operations.
+     *
+     * @see http://php.net/manual/en/language.oop5.magic.php#object.tostring
+     *
+     * @return string
+     */
+    public function __toString()
+    {
+        return $this->content;
+    }
+
+    /**
+     * Closes the stream and any underlying resources.
+     *
+     * @return void
+     */
+    public function close()
+    {
+        $this->content = '';
+        $this->size = -1;
+    }
+
+    /**
+     * Separates any underlying resources from the stream.
+     *
+     * After the stream has been detached, the stream is in an unusable state.
+     *
+     * @return resource|null Underlying PHP stream, if any
+     */
+    public function detach()
+    {
+        return null;
+    }
+
+    /**
+     * Get the size of the stream if known.
+     *
+     * @return int|null returns the size in bytes if known, or null if unknown
+     */
+    public function getSize()
+    {
+        return $this->size;
+    }
+
+    /**
+     * Returns the current position of the file read/write pointer.
+     *
+     * @return int Position of the file pointer
+     *
+     * @throws \RuntimeException on error
+     */
+    public function tell()
+    {
+        return $this->position;
+    }
+
+    /**
+     * Returns true if the stream is at the end of the stream.
+     *
+     * @return bool
+     */
+    public function eof()
+    {
+        return $this->position > $this->size;
+    }
+
+    /**
+     * Returns whether or not the stream is seekable.
+     *
+     * @return bool
+     */
+    public function isSeekable()
+    {
+        return true;
+    }
+
+    /**
+     * Seek to a position in the stream.
+     *
+     * @see http://www.php.net/manual/en/function.fseek.php
+     *
+     * @param int $offset Stream offset
+     * @param int $whence Specifies how the cursor position will be calculated
+     *                    based on the seek offset. Valid values are identical to the built-in
+     *                    PHP $whence values for `fseek()`.  SEEK_SET: Set position equal to
+     *                    offset bytes SEEK_CUR: Set position to current location plus offset
+     *                    SEEK_END: Set position to end-of-stream plus offset.
+     *
+     * @return void
+     *
+     * @throws \RuntimeException on failure
+     */
+    public function seek($offset, $whence = \SEEK_SET)
+    {
+        switch ($whence)
+        {
+            case \SEEK_SET:
+                if ($offset < 0)
+                {
+                    throw new \RuntimeException('offset failure');
+                }
+                $this->position = $offset;
+                break;
+            case \SEEK_CUR:
+                $this->position += $offset;
+                break;
+            case \SEEK_END:
+                $this->position = $this->size - 1 + $offset;
+                break;
+        }
+    }
+
+    /**
+     * Seek to the beginning of the stream.
+     *
+     * If the stream is not seekable, this method will raise an exception;
+     * otherwise, it will perform a seek(0).
+     *
+     * @see seek()
+     * @see http://www.php.net/manual/en/function.fseek.php
+     *
+     * @return void
+     *
+     * @throws \RuntimeException on failure
+     */
+    public function rewind()
+    {
+        $this->position = 0;
+    }
+
+    /**
+     * Returns whether or not the stream is writable.
+     *
+     * @return bool
+     */
+    public function isWritable()
+    {
+        return true;
+    }
+
+    /**
+     * Write data to the stream.
+     *
+     * @param string $string the string that is to be written
+     *
+     * @return int returns the number of bytes written to the stream
+     *
+     * @throws \RuntimeException on failure
+     */
+    public function write($string)
+    {
+        $content = &$this->content;
+        $position = &$this->position;
+        $content = substr_replace($content, $string, $position, 0);
+        $len = \strlen($string);
+        $position += $len;
+        $this->size += $len;
+
+        return $len;
+    }
+
+    /**
+     * Returns whether or not the stream is readable.
+     *
+     * @return bool
+     */
+    public function isReadable()
+    {
+        return true;
+    }
+
+    /**
+     * Read data from the stream.
+     *
+     * @param int $length Read up to $length bytes from the object and return
+     *                    them. Fewer than $length bytes may be returned if underlying stream
+     *                    call returns fewer bytes.
+     *
+     * @return string returns the data read from the stream, or an empty string
+     *                if no bytes are available
+     *
+     * @throws \RuntimeException if an error occurs
+     */
+    public function read($length)
+    {
+        $position = &$this->position;
+        $result = substr($this->content, $position, $length);
+        $position += $length;
+
+        return $result;
+    }
+
+    /**
+     * Returns the remaining contents in a string.
+     *
+     * @return string
+     *
+     * @throws \RuntimeException if unable to read or an error occurs while
+     *                           reading
+     */
+    public function getContents()
+    {
+        $position = &$this->position;
+        if (0 === $position)
+        {
+            $position = $this->size;
+
+            return $this->content;
+        }
+        else
+        {
+            return $this->read($this->size - $position);
+        }
+    }
+
+    /**
+     * Get stream metadata as an associative array or retrieve a specific key.
+     *
+     * The keys returned are identical to the keys returned from PHP's
+     * stream_get_meta_data() function.
+     *
+     * @see http://php.net/manual/en/function.stream-get-meta-data.php
+     *
+     * @param string $key specific metadata to retrieve
+     *
+     * @return array|mixed|null Returns an associative array if no key is
+     *                          provided. Returns a specific key value if a key is provided and the
+     *                          value is found, or null if the key is not found.
+     */
+    public function getMetadata($key = null)
+    {
+        return null;
+    }
+}

+ 53 - 0
vendor/yurunsoft/yurun-http/src/YurunHttp/Stream/StreamMode.php

@@ -0,0 +1,53 @@
+<?php
+
+namespace Yurun\Util\YurunHttp\Stream;
+
+/**
+ * 流访问类型.
+ */
+abstract class StreamMode
+{
+    /**
+     * 只读方式打开,指针指向开头.
+     */
+    const READONLY = 'r';
+
+    /**
+     * 读写方式打开,指针指向开头.
+     */
+    const READ_WRITE = 'r+';
+
+    /**
+     * 写入方式打开,将文件指针指向文件头并将文件大小截为零。如果文件不存在则尝试创建之。
+     */
+    const WRITE_CLEAN = 'w';
+
+    /**
+     * 读写方式打开,将文件指针指向文件头并将文件大小截为零。如果文件不存在则尝试创建之。
+     */
+    const READ_WRITE_CLEAN = 'w+';
+
+    /**
+     * 写入方式打开,将文件指针指向文件末尾。如果文件不存在则尝试创建之。
+     */
+    const WRITE_END = 'a';
+
+    /**
+     * 读写方式打开,将文件指针指向文件末尾。如果文件不存在则尝试创建之。
+     */
+    const READ_WRITE_END = 'a+';
+
+    /**
+     * 创建并以写入方式打开,将文件指针指向文件头。如果文件已存在,则 fopen() 调用失败并返回 FALSE,并生成一条 E_WARNING 级别的错误信息。如果文件不存在则尝试创建之。
+     * 这和给底层的 open(2) 系统调用指定 O_EXCL|O_CREAT 标记是等价的。
+     * 仅能用于本地文件。
+     */
+    const CREATE_WRITE = 'x';
+
+    /**
+     * 创建并以读写方式打开,将文件指针指向文件头。如果文件已存在,则 fopen() 调用失败并返回 FALSE,并生成一条 E_WARNING 级别的错误信息。如果文件不存在则尝试创建之。
+     * 这和给底层的 open(2) 系统调用指定 O_EXCL|O_CREAT 标记是等价的。
+     * 仅能用于本地文件。
+     */
+    const CREATE_READ_WRITE = 'x+';
+}

+ 33 - 0
vendor/yurunsoft/yurun-http/src/YurunHttp/Traits/TCookieManager.php

@@ -0,0 +1,33 @@
+<?php
+
+namespace Yurun\Util\YurunHttp\Traits;
+
+use Yurun\Util\YurunHttp\Cookie\CookieManager;
+
+trait TCookieManager
+{
+    /**
+     * Cookie 管理器.
+     *
+     * @var \Yurun\Util\YurunHttp\Cookie\CookieManager
+     */
+    protected $cookieManager;
+
+    /**
+     * @return void
+     */
+    private function initCookieManager()
+    {
+        $this->cookieManager = new CookieManager();
+    }
+
+    /**
+     * Get cookie 管理器.
+     *
+     * @return \Yurun\Util\YurunHttp\Cookie\CookieManager
+     */
+    public function getCookieManager()
+    {
+        return $this->cookieManager;
+    }
+}

+ 67 - 0
vendor/yurunsoft/yurun-http/src/YurunHttp/Traits/THandler.php

@@ -0,0 +1,67 @@
+<?php
+
+namespace Yurun\Util\YurunHttp\Traits;
+
+use InvalidArgumentException;
+use Psr\Http\Message\UriInterface;
+use Yurun\Util\YurunHttp\Http\Psr7\Uri;
+
+trait THandler
+{
+    /**
+     * 处理重定向的 location.
+     *
+     * @param string       $location
+     * @param UriInterface $currentUri
+     *
+     * @return UriInterface
+     */
+    public function parseRedirectLocation($location, $currentUri)
+    {
+        $locationUri = new Uri($location);
+        if ('' === $locationUri->getHost())
+        {
+            if (!isset($location[0]))
+            {
+                throw new InvalidArgumentException(sprintf('Invalid $location: %s', $location));
+            }
+            if ('/' === $location[0])
+            {
+                $uri = $currentUri->withQuery('')->withPath($location);
+            }
+            else
+            {
+                $path = \dirname($currentUri);
+                if ('\\' === \DIRECTORY_SEPARATOR && false !== strpos($path, \DIRECTORY_SEPARATOR))
+                {
+                    $path = str_replace(\DIRECTORY_SEPARATOR, '/', $path);
+                }
+                $uri = new Uri($path . '/' . $location);
+            }
+        }
+        else
+        {
+            $uri = $locationUri;
+        }
+
+        return $uri;
+    }
+
+    /**
+     * 检查请求对象
+     *
+     * @param \Yurun\Util\YurunHttp\Http\Request[] $requests
+     *
+     * @return void
+     */
+    protected function checkRequests($requests)
+    {
+        foreach ($requests as $request)
+        {
+            if (!$request instanceof \Yurun\Util\YurunHttp\Http\Request)
+            {
+                throw new \InvalidArgumentException('Request must be instance of \Yurun\Util\YurunHttp\Http\Request');
+            }
+        }
+    }
+}

+ 98 - 0
vendor/yurunsoft/yurun-http/src/YurunHttp/WebSocket/IWebSocketClient.php

@@ -0,0 +1,98 @@
+<?php
+
+namespace Yurun\Util\YurunHttp\WebSocket;
+
+interface IWebSocketClient
+{
+    /**
+     * 初始化.
+     *
+     * @param \Yurun\Util\YurunHttp\Handler\IHandler $httpHandler
+     * @param \Yurun\Util\YurunHttp\Http\Request     $request
+     * @param \Yurun\Util\YurunHttp\Http\Response    $response
+     *
+     * @return void
+     */
+    public function init($httpHandler, $request, $response);
+
+    /**
+     * 获取 Http Handler.
+     *
+     * @return \Yurun\Util\YurunHttp\Handler\IHandler
+     */
+    public function getHttpHandler();
+
+    /**
+     * 获取 Http Request.
+     *
+     * @return \Yurun\Util\YurunHttp\Http\Request
+     */
+    public function getHttpRequest();
+
+    /**
+     * 获取 Http Response.
+     *
+     * @return \Yurun\Util\YurunHttp\Http\Response
+     */
+    public function getHttpResponse();
+
+    /**
+     * 连接.
+     *
+     * @return bool
+     */
+    public function connect();
+
+    /**
+     * 关闭连接.
+     *
+     * @return void
+     */
+    public function close();
+
+    /**
+     * 发送数据.
+     *
+     * @param mixed $data
+     *
+     * @return bool
+     */
+    public function send($data);
+
+    /**
+     * 接收数据.
+     *
+     * @param float|null $timeout 超时时间,单位:秒。默认为 null 不限制
+     *
+     * @return mixed
+     */
+    public function recv($timeout = null);
+
+    /**
+     * 是否已连接.
+     *
+     * @return bool
+     */
+    public function isConnected();
+
+    /**
+     * 获取错误码
+     *
+     * @return int
+     */
+    public function getErrorCode();
+
+    /**
+     * 获取错误信息.
+     *
+     * @return string
+     */
+    public function getErrorMessage();
+
+    /**
+     * 获取原始客户端对象
+     *
+     * @return mixed
+     */
+    public function getClient();
+}

+ 193 - 0
vendor/yurunsoft/yurun-http/src/YurunHttp/WebSocket/Swoole.php

@@ -0,0 +1,193 @@
+<?php
+
+namespace Yurun\Util\YurunHttp\WebSocket;
+
+use Yurun\Util\YurunHttp\Attributes;
+use Yurun\Util\YurunHttp\Exception\WebSocketException;
+
+class Swoole implements IWebSocketClient
+{
+    /**
+     * Http Request.
+     *
+     * @var \Yurun\Util\YurunHttp\Http\Request
+     */
+    private $request;
+
+    /**
+     * Http Response.
+     *
+     * @var \Yurun\Util\YurunHttp\Http\Response
+     */
+    private $response;
+
+    /**
+     * Handler.
+     *
+     * @var \Swoole\Coroutine\Http\Client
+     */
+    private $handler;
+
+    /**
+     * Http Handler.
+     *
+     * @var \Yurun\Util\YurunHttp\Handler\Swoole
+     */
+    private $httpHandler;
+
+    /**
+     * 连接状态
+     *
+     * @var bool
+     */
+    private $connected = false;
+
+    /**
+     * 初始化.
+     *
+     * @param \Yurun\Util\YurunHttp\Handler\Swoole $httpHandler
+     * @param \Yurun\Util\YurunHttp\Http\Request   $request
+     * @param \Yurun\Util\YurunHttp\Http\Response  $response
+     *
+     * @return void
+     */
+    public function init($httpHandler, $request, $response)
+    {
+        $this->httpHandler = $httpHandler;
+        $this->request = $request;
+        $this->response = $response;
+        $this->handler = $request->getAttribute(Attributes::PRIVATE_CONNECTION);
+        $this->connected = true;
+    }
+
+    /**
+     * 获取 Http Handler.
+     *
+     * @return \Yurun\Util\YurunHttp\Handler\IHandler
+     */
+    public function getHttpHandler()
+    {
+        return $this->httpHandler;
+    }
+
+    /**
+     * 获取 Http Request.
+     *
+     * @return \Yurun\Util\YurunHttp\Http\Request
+     */
+    public function getHttpRequest()
+    {
+        return $this->request;
+    }
+
+    /**
+     * 获取 Http Response.
+     *
+     * @return \Yurun\Util\YurunHttp\Http\Response
+     */
+    public function getHttpResponse()
+    {
+        return $this->response;
+    }
+
+    /**
+     * 连接.
+     *
+     * @return bool
+     */
+    public function connect()
+    {
+        $this->httpHandler->websocket($this->request, $this);
+
+        return $this->isConnected();
+    }
+
+    /**
+     * 关闭连接.
+     *
+     * @return void
+     */
+    public function close()
+    {
+        $this->handler->close();
+        $this->connected = true;
+    }
+
+    /**
+     * 发送数据.
+     *
+     * @param mixed $data
+     *
+     * @return bool
+     */
+    public function send($data)
+    {
+        $handler = $this->handler;
+        $result = $handler->push($data);
+        if (!$result)
+        {
+            $errCode = $handler->errCode;
+            throw new WebSocketException(sprintf('Send Failed, error: %s, errorCode: %s', swoole_strerror($errCode), $errCode), $errCode);
+        }
+
+        return $result;
+    }
+
+    /**
+     * 接收数据.
+     *
+     * @param float|null $timeout 超时时间,单位:秒。默认为 null 不限制
+     *
+     * @return mixed
+     */
+    public function recv($timeout = null)
+    {
+        $result = $this->handler->recv($timeout);
+        if (!$result)
+        {
+            return false;
+        }
+
+        return $result->data;
+    }
+
+    /**
+     * 是否已连接.
+     *
+     * @return bool
+     */
+    public function isConnected()
+    {
+        return $this->connected;
+    }
+
+    /**
+     * 获取错误码
+     *
+     * @return int
+     */
+    public function getErrorCode()
+    {
+        return $this->handler->errCode;
+    }
+
+    /**
+     * 获取错误信息.
+     *
+     * @return string
+     */
+    public function getErrorMessage()
+    {
+        return $this->handler->errMsg;
+    }
+
+    /**
+     * 获取原始客户端对象
+     *
+     * @return \Swoole\Coroutine\Http\Client
+     */
+    public function getClient()
+    {
+        return $this->handler;
+    }
+}

+ 20 - 0
vendor/yurunsoft/yurun-oauth-login/LICENSE

@@ -0,0 +1,20 @@
+The MIT License (MIT)
+
+Copyright (c) 2017 宇润
+
+Permission is hereby granted, free of charge, to any person obtaining a copy of
+this software and associated documentation files (the "Software"), to deal in
+the Software without restriction, including without limitation the rights to
+use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
+the Software, and to permit persons to whom the Software is furnished to do so,
+subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
+FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
+COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
+IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
+CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

+ 132 - 0
vendor/yurunsoft/yurun-oauth-login/README.md

@@ -0,0 +1,132 @@
+# YurunOAuthLogin
+
+[![Latest Version](https://img.shields.io/packagist/v/yurunsoft/yurun-oauth-login.svg)](https://packagist.org/packages/yurunsoft/yurun-oauth-login)
+[![IMI Doc](https://img.shields.io/badge/docs-passing-green.svg)](http://doc.yurunsoft.com/YurunOAuthLogin)
+[![IMI License](https://img.shields.io/github/license/Yurunsoft/YurunOAuthLogin.svg)](https://github.com/Yurunsoft/YurunOAuthLogin/blob/master/LICENSE)
+
+## 介绍
+
+YurunOAuthLogin是一个PHP 第三方登录授权 SDK,集成了QQ、微信、微博、Github等常用接口。
+
+无框架依赖,支持所有框架,支持 Swoole 协程环境。
+
+我们有完善的在线技术文档:[http://doc.yurunsoft.com/YurunOAuthLogin](http://doc.yurunsoft.com/YurunOAuthLogin)
+
+API 文档:[https://apidoc.gitee.com/yurunsoft/YurunOAuthLogin](https://apidoc.gitee.com/yurunsoft/YurunOAuthLogin)
+
+宇润PHP全家桶群:17916227 [![点击加群](https://pub.idqqimg.com/wpa/images/group.png "点击加群")](https://jq.qq.com/?_wv=1027&k=5wXf4Zq),如有问题会有人解答和修复。
+
+程序员日常划水群:74401592 [![点击加群](https://pub.idqqimg.com/wpa/images/group.png "点击加群")](https://shang.qq.com/wpa/qunwpa?idkey=e2e6b49e9a648aae5285b3aba155d59107bb66fde02e229e078bd7359cac8ac3)。
+
+大家在开发中肯定会对接各种各样的第三方平台,我个人精力有限,欢迎各位来提交 PR ([Github](https://github.com/Yurunsoft/YurunOAuthLogin)),一起完善它,让它能够支持更多的平台,更加好用。
+
+## 支持的登录平台
+
+- QQ、QQ 小程序
+- 微信网页扫码、微信公众号、微信小程序
+- 支付宝网页、支付宝 APP、支付宝小程序
+- 微博
+- 百度
+- Github
+- Gitee
+- Coding
+- 开源中国(OSChina)
+- CSDN
+
+> 后续将不断添加新的平台支持,也欢迎你来提交PR,一起完善!
+
+## 安装
+
+在您的composer.json中加入配置:
+
+`PHP >= 5.5.0`
+
+```json
+{
+    "require": {
+        "yurunsoft/yurun-oauth-login": "~3.0"
+    }
+}
+```
+
+`PHP < 5.5.0`
+
+```json
+{
+    "require": {
+        "yurunsoft/yurun-oauth-login": "~2.0"
+    }
+}
+```
+
+> 3.x 版本支持 PHP >= 5.5,持续迭代维护中
+
+> 2.x 版本支持 PHP >= 5.4,支持长期 BUG 维护,保证稳定可用,停止功能性更新
+
+## 代码实例
+
+自v1.2起所有方法统一参数调用,如果需要额外参数的可使用对象属性赋值,具体参考test目录下的测试代码。
+
+下面代码以QQ接口举例,完全可以把QQ字样改为其它任意接口字样使用。
+
+### 实例化
+
+```php
+$qqOAuth = new \Yurun\OAuthLogin\QQ\OAuth2('appid', 'appkey', 'callbackUrl');
+```
+
+### 登录
+
+```php
+$url = $qqOAuth->getAuthUrl();
+$_SESSION['YURUN_QQ_STATE'] = $qqOAuth->state;
+header('location:' . $url);
+```
+
+### 回调处理
+
+```php
+// 获取accessToken
+$accessToken = $qqOAuth->getAccessToken($_SESSION['YURUN_QQ_STATE']);
+
+// 调用过getAccessToken方法后也可这么获取
+// $accessToken = $qqOAuth->accessToken;
+// 这是getAccessToken的api请求返回结果
+// $result = $qqOAuth->result;
+
+// 用户资料
+$userInfo = $qqOAuth->getUserInfo();
+
+// 这是getAccessToken的api请求返回结果
+// $result = $qqOAuth->result;
+
+// 用户唯一标识
+$openid = $qqOAuth->openid;
+```
+
+### 解决第三方登录只能设置一个回调域名的问题
+
+```php
+// 解决只能设置一个回调域名的问题,下面地址需要改成你项目中的地址,可以参考test/QQ/loginAgent.php写法
+$qqOAuth->loginAgentUrl = 'http://localhost/test/QQ/loginAgent.php';
+
+$url = $qqOAuth->getAuthUrl();
+$_SESSION['YURUN_QQ_STATE'] = $qqOAuth->state;
+header('location:' . $url);
+```
+
+### Swoole 协程环境支持
+
+```php
+\Yurun\Util\YurunHttp::setDefaultHandler('Yurun\Util\YurunHttp\Handler\Swoole');
+```
+
+## 特别鸣谢
+
+* [GetWeixinCode](https://github.com/HADB/GetWeixinCode "GetWeixinCode")
+
+## 捐赠
+
+<img src="https://raw.githubusercontent.com/Yurunsoft/YurunOAuthLogin/master/res/pay.png"/>
+
+开源不求盈利,多少都是心意,生活不易,随缘随缘……

+ 18 - 0
vendor/yurunsoft/yurun-oauth-login/composer.json

@@ -0,0 +1,18 @@
+{
+    "name": "yurunsoft/yurun-oauth-login",
+    "description": "YurunOAuthLogin是一个PHP 第三方登录授权 SDK,集成了QQ、微信、微博、Github等常用接口。",
+    "type": "library",
+    "license": "MIT",
+    "autoload": {
+        "psr-4": {
+            "Yurun\\OAuthLogin\\": "src/"
+        }
+    },
+    "require": {
+        "php": ">=5.5",
+        "yurunsoft/yurun-http" : "~4.0"
+    },
+    "require-dev": {
+        "friendsofphp/php-cs-fixer": "2.18.3"
+    }
+}

+ 285 - 0
vendor/yurunsoft/yurun-oauth-login/src/Alipay/OAuth2.php

@@ -0,0 +1,285 @@
+<?php
+
+namespace Yurun\OAuthLogin\Alipay;
+
+use Yurun\OAuthLogin\ApiException;
+use Yurun\OAuthLogin\Base;
+
+/**
+ * 支付宝授权
+ * 网页授权文档:https://docs.open.alipay.com/53/104114.
+ */
+class OAuth2 extends Base
+{
+    /**
+     * api域名.
+     */
+    const API_DOMAIN = 'https://openapi.alipay.com/gateway.do';
+
+    /**
+     * 非必须参数。接口权限值,目前只支持 auth_userinfo 和 auth_base 两个值。以空格分隔的权限列表,若不传递此参数,代表请求的数据访问操作权限与上次获取Access Token时一致。通过Refresh Token刷新Access Token时所要求的scope权限范围必须小于等于上次获取Access Token时授予的权限范围。
+     *
+     * @var string
+     */
+    public $scope;
+
+    /**
+     * 商户生成签名字符串所使用的签名算法类型,目前支持RSA2和RSA,推荐使用RSA2.
+     *
+     * @var string
+     */
+    public $signType = 'RSA2';
+
+    /**
+     * 详见应用授权概述:https://opendocs.alipay.com/isv/10467/xldcyq.
+     *
+     * @var string
+     */
+    public $appAuthToken;
+
+    /**
+     * 私有证书文件内容.
+     *
+     * @var string
+     */
+    public $appPrivateKey;
+
+    /**
+     * 私有证书文件地址,不为空时优先使用文件地址
+     *
+     * @var string
+     */
+    public $appPrivateKeyFile;
+
+    /**
+     * 第一步:获取登录页面跳转url.
+     *
+     * @param string $callbackUrl 登录回调地址
+     * @param string $state       非必须参数,用于保持请求和回调的状态,授权服务器在回调时(重定向用户浏览器到“redirect_uri”时),会在Query Parameter中原样回传该参数。OAuth2.0标准协议建议,利用state参数来防止CSRF攻击。
+     * @param array  $scope       非必须参数,以空格分隔的权限列表,若不传递此参数,代表请求用户的默认权限。
+     *
+     * @return string
+     */
+    public function getAuthUrl($callbackUrl = null, $state = null, $scope = null)
+    {
+        $option = [
+            'app_id'			      => $this->appid,
+            'scope'				      => $scope ? $scope : 'auth_userinfo',
+            'redirect_uri'		 => null === $callbackUrl ? $this->callbackUrl : $callbackUrl,
+            'state'				      => $this->getState($state),
+        ];
+        if (null === $this->loginAgentUrl)
+        {
+            return 'https://openauth.alipay.com/oauth2/publicAppAuthorize.htm?' . $this->http_build_query($option);
+        }
+        else
+        {
+            return $this->loginAgentUrl . '?' . $this->http_build_query($option);
+        }
+    }
+
+    /**
+     * 第二步:处理回调并获取access_token。与getAccessToken不同的是会验证state值是否匹配,防止csrf攻击。
+     *
+     * @param string $storeState 存储的正确的state
+     * @param string $code       第一步里$redirectUri地址中传过来的code,为null则通过get参数获取
+     * @param string $state      回调接收到的state,为null则通过get参数获取
+     *
+     * @return string
+     */
+    protected function __getAccessToken($storeState, $code = null, $state = null)
+    {
+        $params = [
+            'app_id'		    => $this->appid,
+            'method'		    => 'alipay.system.oauth.token',
+            'charset'		   => 'utf-8',
+            'sign_type'		 => $this->signType,
+            'timestamp'		 => date('Y-m-d H:i:s'),
+            'version'		   => '1.0',
+            'grant_type'	 => 'authorization_code',
+            'code'			     => isset($code) ? $code : (isset($_GET['auth_code']) ? $_GET['auth_code'] : ''),
+        ];
+        if ($this->appAuthToken)
+        {
+            $params['app_auth_token'] = $this->appAuthToken;
+        }
+        $params['sign'] = $this->sign($params);
+        $response = $this->http->get(static::API_DOMAIN, $params);
+        $this->result = $response->json(true);
+
+        if (!isset($this->result['alipay_system_oauth_token_response']) && isset($this->result['error_response']))
+        {
+            throw new ApiException(sprintf('%s %s', $this->result['error_response']['msg'], $this->result['error_response']['sub_msg']), $this->result['error_response']['code']);
+        }
+        $this->result = $responseData = $this->result['alipay_system_oauth_token_response'];
+        if (isset($responseData['code']))
+        {
+            throw new ApiException(sprintf('%s %s', $responseData['msg'], $responseData['sub_msg']), $responseData['code']);
+        }
+        $this->openid = $responseData['user_id'];
+
+        return $this->accessToken = $responseData['access_token'];
+    }
+
+    /**
+     * 获取用户资料.
+     *
+     * @param string $accessToken
+     *
+     * @return array
+     */
+    public function getUserInfo($accessToken = null)
+    {
+        $params = [
+            'app_id'		    => $this->appid,
+            'method'		    => 'alipay.user.userinfo.share',
+            'charset'		   => 'utf-8',
+            'sign_type'		 => $this->signType,
+            'timestamp'		 => date('Y-m-d H:i:s'),
+            'version'		   => '1.0',
+            'auth_token'	 => null === $accessToken ? $this->accessToken : $accessToken,
+        ];
+        if ($this->appAuthToken)
+        {
+            $params['app_auth_token'] = $this->appAuthToken;
+        }
+        $params['sign'] = $this->sign($params);
+        $response = $this->http->get(static::API_DOMAIN, $params);
+        $this->result = $response->json(true);
+
+        if (!isset($this->result['alipay_user_userinfo_share_response']) && isset($this->result['error_response']))
+        {
+            throw new ApiException(sprintf('%s %s', $this->result['error_response']['msg'], $this->result['error_response']['sub_msg']), $this->result['error_response']['code']);
+        }
+        $this->result = $responseData = $this->result['alipay_user_userinfo_share_response'];
+        if (isset($responseData['code']) && 10000 != $responseData['code'])
+        {
+            throw new ApiException(sprintf('%s %s', $responseData['msg'], $responseData['sub_msg']), $responseData['code']);
+        }
+
+        return $responseData;
+    }
+
+    /**
+     * 刷新AccessToken续期
+     *
+     * @param string $refreshToken
+     *
+     * @return bool
+     */
+    public function refreshToken($refreshToken)
+    {
+        $params = [
+            'app_id'		       => $this->appid,
+            'method'		       => 'alipay.system.oauth.token',
+            'charset'		      => 'utf-8',
+            'sign_type'		    => $this->signType,
+            'timestamp'		    => date('Y-m-d H:i:s'),
+            'version'		      => '1.0',
+            'grant_type'	    => 'refresh_token',
+            'refresh_token'	 => $refreshToken,
+        ];
+        if ($this->appAuthToken)
+        {
+            $params['app_auth_token'] = $this->appAuthToken;
+        }
+        $params['sign'] = $this->sign($params);
+        $response = $this->http->get(static::API_DOMAIN, $params);
+        $this->result = $response->json(true);
+
+        if (!isset($this->result['alipay_system_oauth_token_response']) && isset($this->result['error_response']))
+        {
+            throw new ApiException(sprintf('%s %s', $this->result['error_response']['msg'], $this->result['error_response']['sub_msg']), $this->result['error_response']['code']);
+        }
+        $this->result = $responseData = $this->result['alipay_system_oauth_token_response'];
+        if (isset($responseData['code']))
+        {
+            throw new ApiException(sprintf('%s %s', $responseData['msg'], $responseData['sub_msg']), $responseData['code']);
+        }
+        $this->openid = $responseData['user_id'];
+
+        return $this->accessToken = $responseData['access_token'];
+    }
+
+    /**
+     * 检验授权凭证AccessToken是否有效.
+     *
+     * @param string $accessToken
+     *
+     * @return bool
+     */
+    public function validateAccessToken($accessToken = null)
+    {
+        try
+        {
+            $this->getUserInfo($accessToken);
+
+            return true;
+        }
+        catch (ApiException $e)
+        {
+            return false;
+        }
+    }
+
+    /**
+     * 签名.
+     *
+     * @param $data
+     *
+     * @return string
+     */
+    public function sign($data)
+    {
+        $content = $this->parseSignData($data);
+        if (empty($this->appPrivateKeyFile))
+        {
+            $key = $this->appPrivateKey;
+            $method = 'signPrivate';
+        }
+        else
+        {
+            $key = $this->appPrivateKeyFile;
+            $method = 'signPrivateFromFile';
+        }
+        switch ($this->signType)
+        {
+            case 'RSA':
+                $result = \Yurun\OAuthLogin\Lib\RSA::$method($content, $key);
+                break;
+            case 'RSA2':
+                $result = \Yurun\OAuthLogin\Lib\RSA2::$method($content, $key);
+                break;
+            default:
+                throw new \Exception('未知的加密方式:' . $this->signType);
+        }
+
+        return base64_encode($result);
+    }
+
+    /**
+     * 处理验证数据.
+     *
+     * @param array $data
+     *
+     * @return string
+     */
+    public function parseSignData($data)
+    {
+        if (isset($data['sign']))
+        {
+            unset($data['sign']);
+        }
+        ksort($data);
+        $content = '';
+        foreach ($data as $k => $v)
+        {
+            if ('' !== $v && null !== $v && !\is_array($v))
+            {
+                $content .= $k . '=' . $v . '&';
+            }
+        }
+
+        return trim($content, '&');
+    }
+}

+ 85 - 0
vendor/yurunsoft/yurun-oauth-login/src/Alipay/loginAgent.html

@@ -0,0 +1,85 @@
+<!DOCTYPE html>
+<html>
+
+    <head>
+        <meta charset="UTF-8">
+        <title>支付宝登录</title>
+    </head>
+
+    <body>
+        <script>
+            var GWC = {
+                version: '1.0.0',
+                urlParams: {},
+                appendParams: function(url, params) {
+                    if (params) {
+                        var baseWithSearch = url.split('#')[0];
+                        var hash = url.split('#')[1];
+                        for (var key in params) {
+                            var attrValue = params[key];
+                            if (attrValue !== undefined) {
+                                var newParam = key + "=" + attrValue;
+                                if (baseWithSearch.indexOf('?') > 0) {
+                                    var oldParamReg = new RegExp('^' + key + '=[-%.!~*\'\(\)\\w]*', 'g');
+                                    if (oldParamReg.test(baseWithSearch)) {
+                                        baseWithSearch = baseWithSearch.replace(oldParamReg, newParam);
+                                    } else {
+                                        baseWithSearch += "&" + newParam;
+                                    }
+                                } else {
+                                    baseWithSearch += "?" + newParam;
+                                }
+                            }
+                        }
+
+                        if (hash) {
+                            url = baseWithSearch + '#' + hash;
+                        } else {
+                            url = baseWithSearch;
+                        }
+                    }
+                    return url;
+                },
+                getUrlParams: function() {
+                    var pairs = location.search.substring(1).split('&');
+                    for (var i = 0; i < pairs.length; i++) {
+                        var pos = pairs[i].indexOf('=');
+                        if (pos === -1) {
+                            continue;
+                        }
+                        GWC.urlParams[pairs[i].substring(0, pos)] = decodeURIComponent(pairs[i].substring(pos + 1));
+                    }
+                },
+                doRedirect: function() {
+					var code = GWC.urlParams['code'];
+                    var appId = GWC.urlParams['app_id'];
+                    var scope = GWC.urlParams['scope'] || 'auth_userinfo';
+                    var state = GWC.urlParams['state'];
+                    var redirectUri;
+
+                    if (!code) {
+                        //第一步,没有拿到code,跳转至支付宝授权页面获取code
+                        redirectUri = GWC.appendParams('https://openauth.alipay.com/oauth2/publicAppAuthorize.htm', {
+                            'app_id': appId,
+                            'redirect_uri': encodeURIComponent(location.href),
+                            'scope': scope,
+                            'state': state,
+                        });
+                    } else {
+                        //第二步,从支付宝授权页面跳转回来,已经获取到了code,再次跳转到实际所需页面
+                        redirectUri = GWC.appendParams(GWC.urlParams['redirect_uri'], {
+                            'code': code,
+                            'state': state
+                        });
+                    }
+
+                    location.href = redirectUri;
+                }
+            };
+
+            GWC.getUrlParams();
+            GWC.doRedirect();
+        </script>
+    </body>
+
+</html>

+ 7 - 0
vendor/yurunsoft/yurun-oauth-login/src/ApiException.php

@@ -0,0 +1,7 @@
+<?php
+
+namespace Yurun\OAuthLogin;
+
+class ApiException extends \Exception
+{
+}

+ 196 - 0
vendor/yurunsoft/yurun-oauth-login/src/Baidu/OAuth2.php

@@ -0,0 +1,196 @@
+<?php
+
+namespace Yurun\OAuthLogin\Baidu;
+
+use Yurun\OAuthLogin\ApiException;
+use Yurun\OAuthLogin\Base;
+
+class OAuth2 extends Base
+{
+    /**
+     * api域名.
+     */
+    const API_DOMAIN = 'https://openapi.baidu.com/';
+
+    /**
+     * 非必须参数,登录和授权页面的展现样式,默认为“page”,具体参数定义请参考http://developer.baidu.com/wiki/index.php?title=docs/oauth/set.
+     *
+     * @var string
+     */
+    public $display;
+
+    /**
+     * 非必须参数,如传递“force_login=1”,则加载登录页时强制用户输入用户名和口令,不会从cookie中读取百度用户的登陆状态。
+     *
+     * @var string
+     */
+    public $forceLogin;
+
+    /**
+     * 非必须参数,如传递“confirm_login=1”且百度用户已处于登陆状态,会提示是否使用已当前登陆用户对应用授权。
+     *
+     * @var string
+     */
+    public $confirmLogin;
+
+    /**
+     * 非必须参数,如传递“login_type=sms”,授权页面会默认使用短信动态密码注册登陆方式。
+     *
+     * @var string
+     */
+    public $loginType;
+
+    /**
+     * 非必须参数。以空格分隔的权限列表,若不传递此参数,代表请求的数据访问操作权限与上次获取Access Token时一致。通过Refresh Token刷新Access Token时所要求的scope权限范围必须小于等于上次获取Access Token时授予的权限范围。关于权限的具体信息请参考http://developer.baidu.com/wiki/index.php?title=docs/oauth/baiduoauth/list.
+     *
+     * @var string
+     */
+    public $scope;
+
+    /**
+     * 获取url地址
+     *
+     * @param string $name   跟在域名后的文本
+     * @param array  $params GET参数
+     *
+     * @return string
+     */
+    public function getUrl($name, $params = [])
+    {
+        return static::API_DOMAIN . $name . (empty($params) ? '' : ('?' . $this->http_build_query($params)));
+    }
+
+    /**
+     * 第一步:获取登录页面跳转url.
+     *
+     * @param string $callbackUrl 登录回调地址
+     * @param string $state       非必须参数,用于保持请求和回调的状态,授权服务器在回调时(重定向用户浏览器到“redirect_uri”时),会在Query Parameter中原样回传该参数。OAuth2.0标准协议建议,利用state参数来防止CSRF攻击。
+     * @param array  $scope       非必须参数,以空格分隔的权限列表,若不传递此参数,代表请求用户的默认权限。关于权限的具体信息请参考“权限列表”。
+     *
+     * @return string
+     */
+    public function getAuthUrl($callbackUrl = null, $state = null, $scope = null)
+    {
+        $option = [
+            'client_id'			    => $this->appid,
+            'response_type'		 => 'code',
+            'redirect_uri'		  => null === $callbackUrl ? $this->callbackUrl : $callbackUrl,
+            'scope'				       => $scope,
+            'state'				       => $this->getState($state),
+            'display'			      => $this->display,
+            'force_login'		   => $this->forceLogin,
+            'confirm_login'		 => $this->confirmLogin,
+            'login_type'		    => $this->loginType,
+        ];
+        if (null === $this->loginAgentUrl)
+        {
+            return $this->getUrl('oauth/2.0/authorize', $option);
+        }
+        else
+        {
+            return $this->loginAgentUrl . '?' . $this->http_build_query($option);
+        }
+    }
+
+    /**
+     * 第二步:处理回调并获取access_token。与getAccessToken不同的是会验证state值是否匹配,防止csrf攻击。
+     *
+     * @param string $storeState 存储的正确的state
+     * @param string $code       第一步里$redirectUri地址中传过来的code,为null则通过get参数获取
+     * @param string $state      回调接收到的state,为null则通过get参数获取
+     *
+     * @return string
+     */
+    protected function __getAccessToken($storeState, $code = null, $state = null)
+    {
+        $response = $this->http->get($this->getUrl('oauth/2.0/token'), [
+            'grant_type'	    => 'authorization_code',
+            'code'			        => isset($code) ? $code : (isset($_GET['code']) ? $_GET['code'] : ''),
+            'client_id'		    => $this->appid,
+            'client_secret'	 => $this->appSecret,
+            'redirect_uri'	  => $this->getRedirectUri(),
+        ]);
+        $this->result = $response->json(true);
+        if (!isset($this->result['error_description']))
+        {
+            return $this->accessToken = $this->result['access_token'];
+        }
+        else
+        {
+            throw new ApiException(isset($this->result['error_description']) ? $this->result['error_description'] : '', $response->httpCode());
+        }
+    }
+
+    /**
+     * 获取用户资料.
+     *
+     * @param string $accessToken
+     *
+     * @return array
+     */
+    public function getUserInfo($accessToken = null)
+    {
+        $response = $this->http->get($this->getUrl('rest/2.0/passport/users/getLoggedInUser', [
+            'access_token'	 => null === $accessToken ? $this->accessToken : $accessToken,
+        ]));
+        $this->result = $response->json(true);
+        if (!isset($this->result['error_description']))
+        {
+            $this->openid = $this->result['uid'];
+
+            return $this->result;
+        }
+        else
+        {
+            throw new ApiException(isset($this->result['error_description']) ? $this->result['error_description'] : '', $response->httpCode());
+        }
+    }
+
+    /**
+     * 刷新AccessToken续期
+     *
+     * @param string $refreshToken
+     *
+     * @return bool
+     */
+    public function refreshToken($refreshToken)
+    {
+        $response = $this->http->get($this->getUrl('oauth/2.0/token'), [
+            'grant_type'	    => 'refresh_token',
+            'refresh_token'	 => $refreshToken,
+            'client_id'		    => $this->appid,
+            'client_secret'	 => $this->appSecret,
+            'scope'			       => $this->scope,
+        ]);
+        $this->result = $response->json(true);
+        if (!isset($this->result['error_description']))
+        {
+            return $this->accessToken = $this->result['access_token'];
+        }
+        else
+        {
+            throw new ApiException(isset($this->result['error_description']) ? $this->result['error_description'] : '', $response->httpCode());
+        }
+    }
+
+    /**
+     * 检验授权凭证AccessToken是否有效.
+     *
+     * @param string $accessToken
+     *
+     * @return bool
+     */
+    public function validateAccessToken($accessToken = null)
+    {
+        try
+        {
+            $this->getUserInfo($accessToken);
+
+            return true;
+        }
+        catch (ApiException $e)
+        {
+            return false;
+        }
+    }
+}

+ 96 - 0
vendor/yurunsoft/yurun-oauth-login/src/Baidu/loginAgent.html

@@ -0,0 +1,96 @@
+<!DOCTYPE html>
+<html>
+
+    <head>
+        <meta charset="UTF-8">
+        <title>百度登录</title>
+    </head>
+
+    <body>
+        <script>
+            var GWC = {
+                version: '1.0.0',
+                urlParams: {},
+                appendParams: function(url, params) {
+                    if (params) {
+                        var baseWithSearch = url.split('#')[0];
+                        var hash = url.split('#')[1];
+                        for (var key in params) {
+                            var attrValue = params[key];
+                            if (attrValue !== undefined) {
+                                var newParam = key + "=" + attrValue;
+                                if (baseWithSearch.indexOf('?') > 0) {
+                                    var oldParamReg = new RegExp('^' + key + '=[-%.!~*\'\(\)\\w]*', 'g');
+                                    if (oldParamReg.test(baseWithSearch)) {
+                                        baseWithSearch = baseWithSearch.replace(oldParamReg, newParam);
+                                    } else {
+                                        baseWithSearch += "&" + newParam;
+                                    }
+                                } else {
+                                    baseWithSearch += "?" + newParam;
+                                }
+                            }
+                        }
+
+                        if (hash) {
+                            url = baseWithSearch + '#' + hash;
+                        } else {
+                            url = baseWithSearch;
+                        }
+                    }
+                    return url;
+                },
+                getUrlParams: function() {
+                    var pairs = location.search.substring(1).split('&');
+                    for (var i = 0; i < pairs.length; i++) {
+                        var pos = pairs[i].indexOf('=');
+                        if (pos === -1) {
+                            continue;
+                        }
+                        GWC.urlParams[pairs[i].substring(0, pos)] = decodeURIComponent(pairs[i].substring(pos + 1));
+                    }
+                },
+                doRedirect: function() {
+					var code = GWC.urlParams['code'];
+                    var appId = GWC.urlParams['client_id'];
+                    var response_type = GWC.urlParams['response_type'];
+                    var redirectUri;
+                    var scope = GWC.urlParams['scope'];
+                    var state = GWC.urlParams['state'];
+                    var display = GWC.urlParams['display'];
+                    var force_login = GWC.urlParams['force_login'];
+                    var confirm_login = GWC.urlParams['confirm_login'];
+                    var login_type = GWC.urlParams['login_type'];
+                    if (!code) {
+                        //第一步,没有拿到code,跳转至授权页面获取code
+                        redirectUri = GWC.appendParams('https://openapi.baidu.com/oauth/2.0/authorize', {
+                            client_id: appId,
+                            response_type: response_type,
+                            redirect_uri: encodeURIComponent(GWC.appendParams(location.href.split('?')[0], {
+                                'redirect_uri': encodeURIComponent(GWC.urlParams['redirect_uri']),
+                            })),
+                            scope: scope,
+                            state: state,
+                            display: display,
+                            force_login: force_login,
+                            confirm_login: confirm_login,
+                            login_type: login_type
+                        });
+                    } else {
+                        //第二步,从授权页面跳转回来,已经获取到了code,再次跳转到实际所需页面
+                        redirectUri = GWC.appendParams(GWC.urlParams['redirect_uri'], {
+                            code: code,
+                            state: state,
+                        });
+                    }
+
+                    location.href = redirectUri;
+                }
+            };
+
+            GWC.getUrlParams();
+            GWC.doRedirect();
+        </script>
+    </body>
+
+</html>

+ 280 - 0
vendor/yurunsoft/yurun-oauth-login/src/Base.php

@@ -0,0 +1,280 @@
+<?php
+
+namespace Yurun\OAuthLogin;
+
+use Yurun\Util\HttpRequest;
+
+abstract class Base
+{
+    /**
+     * http请求类.
+     *
+     * @var Yurun\Util\HttpRequest
+     */
+    public $http;
+
+    /**
+     * 应用的唯一标识。
+     *
+     * @var string
+     */
+    public $appid;
+
+    /**
+     * appid对应的密钥.
+     *
+     * @var string
+     */
+    public $appSecret;
+
+    /**
+     * 登录回调地址
+     *
+     * @var string
+     */
+    public $callbackUrl;
+
+    /**
+     * state值,调用getAuthUrl方法后可以获取到.
+     *
+     * @var string
+     */
+    public $state;
+
+    /**
+     * 授权权限列表.
+     *
+     * @var array
+     */
+    public $scope;
+
+    /**
+     * 接口调用结果.
+     *
+     * @var array
+     */
+    public $result;
+
+    /**
+     * AccessToken,调用相应方法后可以获取到.
+     *
+     * @var string
+     */
+    public $accessToken;
+
+    /**
+     * openid,调用相应方法后可以获取到.
+     *
+     * @var string
+     */
+    public $openid;
+
+    /**
+     * 登录代理地址,用于解决只能设置一个回调域名/地址的问题.
+     *
+     * @var string
+     */
+    public $loginAgentUrl;
+
+    /**
+     * 构造方法.
+     *
+     * @param string $appid       应用的唯一标识
+     * @param string $appSecret   appid对应的密钥
+     * @param string $callbackUrl 登录回调地址
+     */
+    public function __construct($appid = null, $appSecret = null, $callbackUrl = null)
+    {
+        $this->appid = $appid;
+        $this->appSecret = $appSecret;
+        $this->callbackUrl = $callbackUrl;
+        $this->http = new HttpRequest();
+    }
+
+    /**
+     * 把jsonp转为php数组.
+     *
+     * @param string $jsonp jsonp字符串
+     * @param bool   $assoc 当该参数为true时,将返回array而非object
+     *
+     * @return array
+     */
+    public function jsonp_decode($jsonp, $assoc = false)
+    {
+        $jsonp = trim($jsonp);
+        if (isset($jsonp[0]) && '[' !== $jsonp[0] && '{' !== $jsonp[0])
+        {
+            $begin = strpos($jsonp, '(');
+            if (false !== $begin)
+            {
+                $end = strrpos($jsonp, ')');
+                if (false !== $end)
+                {
+                    $jsonp = substr($jsonp, $begin + 1, $end - $begin - 1);
+                }
+            }
+        }
+
+        return json_decode($jsonp, $assoc);
+    }
+
+    /**
+     * http_build_query — 生成 URL-encode 之后的请求字符串.
+     *
+     * @param array  $query_data
+     * @param string $numeric_prefix
+     * @param string $arg_separator
+     * @param int    $enc_type
+     *
+     * @return void
+     */
+    public function http_build_query($query_data, $numeric_prefix = '', $arg_separator = '&', $enc_type = \PHP_QUERY_RFC1738)
+    {
+        return http_build_query($query_data, $numeric_prefix, $arg_separator, $enc_type);
+    }
+
+    /**
+     * 获取state值
+     *
+     * @param string $state
+     *
+     * @return string
+     */
+    protected function getState($state = null)
+    {
+        if (null === $state)
+        {
+            if (null === $this->state)
+            {
+                $this->state = md5(uniqid('', true));
+            }
+        }
+        else
+        {
+            $this->state = $state;
+        }
+
+        return $this->state;
+    }
+
+    /**
+     * 检测state是否相等.
+     *
+     * @param string $storeState 本地存储的正确的state
+     * @param string $state      回调传递过来的state
+     *
+     * @return bool
+     */
+    public function checkState($storeState, $state = null)
+    {
+        if (null === $state)
+        {
+            if (null === $this->state)
+            {
+                if (isset($_GET['state']))
+                {
+                    $state = $_GET['state'];
+                }
+                else
+                {
+                    $state = '';
+                }
+            }
+            else
+            {
+                $state = $this->state;
+            }
+        }
+
+        return $storeState === $state;
+    }
+
+    /**
+     * 第一步:获取登录页面跳转url.
+     *
+     * @param string $callbackUrl 登录回调地址
+     * @param string $state       状态值,不传则自动生成,随后可以通过->state获取。用于第三方应用防止CSRF攻击,成功授权后回调时会原样带回。一般为每个用户登录时随机生成state存在session中,登录回调中判断state是否和session中相同
+     * @param array  $scope       请求用户授权时向用户显示的可进行授权的列表。可空
+     *
+     * @return string
+     */
+    abstract public function getAuthUrl($callbackUrl = null, $state = null, $scope = null);
+
+    /**
+     * 第二步:处理回调并获取access_token。与getAccessToken不同的是会验证state值是否匹配,防止csrf攻击。
+     *
+     * @param string $storeState 存储的正确的state
+     * @param string $code       第一步里$callbackUrl地址中传过来的code,为null则通过get参数获取
+     * @param string $state      回调接收到的state,为null则通过get参数获取
+     *
+     * @return string
+     */
+    public function getAccessToken($storeState = '', $code = null, $state = null)
+    {
+        if (!$this->checkState($storeState, $state))
+        {
+            throw new \InvalidArgumentException('state验证失败');
+        }
+
+        return $this->__getAccessToken($storeState, $code, $state);
+    }
+
+    /**
+     * 第二步:处理回调并获取access_token。与getAccessToken不同的是会验证state值是否匹配,防止csrf攻击。
+     *
+     * @param string $storeState 存储的正确的state
+     * @param string $code       第一步里$callbackUrl地址中传过来的code,为null则通过get参数获取
+     * @param string $state      回调接收到的state,为null则通过get参数获取
+     *
+     * @return string
+     */
+    abstract protected function __getAccessToken($storeState, $code = null, $state = null);
+
+    /**
+     * 获取用户资料.
+     *
+     * @param string $accessToken
+     *
+     * @return array
+     */
+    abstract public function getUserInfo($accessToken = null);
+
+    /**
+     * 刷新AccessToken续期
+     *
+     * @param string $refreshToken
+     *
+     * @return bool
+     */
+    abstract public function refreshToken($refreshToken);
+
+    /**
+     * 检验授权凭证AccessToken是否有效.
+     *
+     * @param string $accessToken
+     *
+     * @return bool
+     */
+    abstract public function validateAccessToken($accessToken = null);
+
+    /**
+     * 输出登录代理页内容,用于解决只能设置一个回调域名/地址的问题.
+     *
+     * @return void
+     */
+    public function displayLoginAgent()
+    {
+        $ref = new \ReflectionClass(static::class);
+        echo file_get_contents(\dirname($ref->getFileName()) . '/loginAgent.html');
+    }
+
+    /**
+     * 获取回调地址
+     *
+     * @return string
+     */
+    public function getRedirectUri()
+    {
+        return null === $this->loginAgentUrl ? $this->callbackUrl : ($this->loginAgentUrl . '?' . http_build_query(['redirect_uri' => $this->callbackUrl]));
+    }
+}

+ 169 - 0
vendor/yurunsoft/yurun-oauth-login/src/CSDN/OAuth2.php

@@ -0,0 +1,169 @@
+<?php
+
+namespace Yurun\OAuthLogin\CSDN;
+
+use Yurun\OAuthLogin\ApiException;
+use Yurun\OAuthLogin\Base;
+
+class OAuth2 extends Base
+{
+    /**
+     * api域名.
+     */
+    const API_DOMAIN = 'http://api.csdn.net/';
+
+    /**
+     * 获取url地址
+     *
+     * @param string $name   跟在域名后的文本
+     * @param array  $params GET参数
+     *
+     * @return string
+     */
+    public function getUrl($name, $params = [])
+    {
+        return static::API_DOMAIN . $name . (empty($params) ? '' : ('?' . $this->http_build_query($params)));
+    }
+
+    /**
+     * 使用账号密码方式登录授权.
+     *
+     * @param string $username 用户名
+     * @param string $password 密码
+     *
+     * @return void
+     */
+    public function login($username, $password)
+    {
+        $response = $this->http->get($this->getUrl('oauth2/access_token'), [
+            'grant_type'	    => 'password ',
+            'username'		     => $username,
+            'password'		     => $password,
+            'client_id'		    => $this->appid,
+            'client_secret'	 => $this->appSecret,
+        ]);
+        $this->result = $response->json(true);
+        if (!isset($this->result['error_code']))
+        {
+            return $this->accessToken = $this->result['access_token'];
+        }
+        else
+        {
+            throw new ApiException(isset($this->result['error']) ? $this->result['error'] : '', $this->result['error_code']);
+        }
+    }
+
+    /**
+     * 第一步:获取登录页面跳转url.
+     *
+     * @param string $callbackUrl 登录回调地址
+     * @param string $state       无用
+     * @param array  $scope       无用
+     *
+     * @return string
+     */
+    public function getAuthUrl($callbackUrl = null, $state = null, $scope = null)
+    {
+        $option = [
+            'client_id'			    => $this->appid,
+            'redirect_uri'		  => null === $callbackUrl ? $this->callbackUrl : $callbackUrl,
+            'response_type'		 => 'code',
+        ];
+        if (null === $this->loginAgentUrl)
+        {
+            return $this->getUrl('oauth2/authorize', $option);
+        }
+        else
+        {
+            return $this->loginAgentUrl . '?' . $this->http_build_query($option);
+        }
+    }
+
+    /**
+     * 第二步:处理回调并获取access_token。与getAccessToken不同的是会验证state值是否匹配,防止csrf攻击。
+     *
+     * @param string $storeState 存储的正确的state
+     * @param string $code       第一步里$redirectUri地址中传过来的code,为null则通过get参数获取
+     * @param string $state      回调接收到的state,为null则通过get参数获取
+     *
+     * @return string
+     */
+    protected function __getAccessToken($storeState, $code = null, $state = null)
+    {
+        $response = $this->http->get($this->getUrl('oauth2/access_token'), [
+            'grant_type'	    => 'authorization_code',
+            'code'			        => isset($code) ? $code : (isset($_GET['code']) ? $_GET['code'] : ''),
+            'client_id'		    => $this->appid,
+            'client_secret'	 => $this->appSecret,
+            'redirect_uri'	  => $this->getRedirectUri(),
+        ]);
+        $this->result = $response->json(true);
+        if (!isset($this->result['error_code']))
+        {
+            return $this->accessToken = $this->result['access_token'];
+        }
+        else
+        {
+            throw new ApiException(isset($this->result['error']) ? $this->result['error'] : '', $this->result['error_code']);
+        }
+    }
+
+    /**
+     * 获取用户资料.
+     *
+     * @param string $accessToken
+     *
+     * @return array
+     */
+    public function getUserInfo($accessToken = null)
+    {
+        $response = $this->http->get($this->getUrl('user/getinfo', [
+            'access_token'	 => null === $accessToken ? $this->accessToken : $accessToken,
+        ]));
+        $this->result = $response->json(true);
+        if (!isset($this->result['error_code']))
+        {
+            $this->openid = $this->result['username'];
+
+            return $this->result;
+        }
+        else
+        {
+            throw new ApiException(isset($this->result['error']) ? $this->result['error'] : '', $this->result['error_code']);
+        }
+    }
+
+    /**
+     * 刷新AccessToken续期
+     *
+     * @param string $refreshToken
+     *
+     * @return bool
+     */
+    public function refreshToken($refreshToken)
+    {
+        // 不支持
+        return false;
+    }
+
+    /**
+     * 检验授权凭证AccessToken是否有效.
+     *
+     * @param string $accessToken
+     *
+     * @return bool
+     */
+    public function validateAccessToken($accessToken = null)
+    {
+        try
+        {
+            $this->getUserInfo($accessToken);
+
+            return true;
+        }
+        catch (ApiException $e)
+        {
+            return false;
+        }
+    }
+}

+ 84 - 0
vendor/yurunsoft/yurun-oauth-login/src/CSDN/loginAgent.html

@@ -0,0 +1,84 @@
+<!DOCTYPE html>
+<html>
+
+    <head>
+        <meta charset="UTF-8">
+        <title>CSDN登录</title>
+    </head>
+
+    <body>
+        <script>
+            var GWC = {
+                version: '1.0.0',
+                urlParams: {},
+                appendParams: function(url, params) {
+                    if (params) {
+                        var baseWithSearch = url.split('#')[0];
+                        var hash = url.split('#')[1];
+                        for (var key in params) {
+                            var attrValue = params[key];
+                            if (attrValue !== undefined) {
+                                var newParam = key + "=" + attrValue;
+                                if (baseWithSearch.indexOf('?') > 0) {
+                                    var oldParamReg = new RegExp('^' + key + '=[-%.!~*\'\(\)\\w]*', 'g');
+                                    if (oldParamReg.test(baseWithSearch)) {
+                                        baseWithSearch = baseWithSearch.replace(oldParamReg, newParam);
+                                    } else {
+                                        baseWithSearch += "&" + newParam;
+                                    }
+                                } else {
+                                    baseWithSearch += "?" + newParam;
+                                }
+                            }
+                        }
+
+                        if (hash) {
+                            url = baseWithSearch + '#' + hash;
+                        } else {
+                            url = baseWithSearch;
+                        }
+                    }
+                    return url;
+                },
+                getUrlParams: function() {
+                    var pairs = location.search.substring(1).split('&');
+                    for (var i = 0; i < pairs.length; i++) {
+                        var pos = pairs[i].indexOf('=');
+                        if (pos === -1) {
+                            continue;
+                        }
+                        GWC.urlParams[pairs[i].substring(0, pos)] = decodeURIComponent(pairs[i].substring(pos + 1));
+                    }
+                },
+                doRedirect: function() {
+					var code = GWC.urlParams['code'];
+                    var appId = GWC.urlParams['client_id'];
+                    var response_type = GWC.urlParams['response_type'];
+                    var redirectUri;
+
+                    if (!code) {
+                        //第一步,没有拿到code,跳转至授权页面获取code
+                        redirectUri = GWC.appendParams('http://api.csdn.net/oauth2/authorize', {
+                            'client_id': appId,
+                            'redirect_uri': encodeURIComponent(GWC.appendParams(location.href.split('?')[0], {
+                                'redirect_uri': encodeURIComponent(GWC.urlParams['redirect_uri']),
+                            })),
+                            'response_type': response_type,
+                        });
+                    } else {
+                        //第二步,从授权页面跳转回来,已经获取到了code,再次跳转到实际所需页面
+                        redirectUri = GWC.appendParams(GWC.urlParams['redirect_uri'], {
+                            'code': code,
+                        });
+                    }
+
+                    location.href = redirectUri;
+                }
+            };
+
+            GWC.getUrlParams();
+            GWC.doRedirect();
+        </script>
+    </body>
+
+</html>

+ 164 - 0
vendor/yurunsoft/yurun-oauth-login/src/Coding/OAuth2.php

@@ -0,0 +1,164 @@
+<?php
+
+namespace Yurun\OAuthLogin\Coding;
+
+use Yurun\OAuthLogin\ApiException;
+use Yurun\OAuthLogin\Base;
+
+class OAuth2 extends Base
+{
+    /**
+     * api域名.
+     */
+    const API_DOMAIN = 'https://coding.net/';
+
+    /**
+     * 获取url地址
+     *
+     * @param string $name   跟在域名后的文本
+     * @param array  $params GET参数
+     *
+     * @return string
+     */
+    public function getUrl($name, $params = [])
+    {
+        return static::API_DOMAIN . $name . (empty($params) ? '' : ('?' . $this->http_build_query($params)));
+    }
+
+    /**
+     * 第一步:获取登录页面跳转url.
+     *
+     * @param string $callbackUrl 登录回调地址
+     * @param string $state       coding无用
+     * @param array  $scope       请求用户授权时向用户显示的可进行授权的列表,多个用逗号分隔
+     *
+     * @return string
+     */
+    public function getAuthUrl($callbackUrl = null, $state = null, $scope = null)
+    {
+        $option = [
+            'client_id'			    => $this->appid,
+            'redirect_uri'		  => null === $callbackUrl ? $this->callbackUrl : $callbackUrl,
+            'response_type'		 => 'code',
+            'scope'				       => $scope,
+        ];
+        if (null === $this->loginAgentUrl)
+        {
+            return $this->getUrl('oauth_authorize.html', $option);
+        }
+        else
+        {
+            return $this->loginAgentUrl . '?' . $this->http_build_query($option);
+        }
+    }
+
+    /**
+     * 第二步:处理回调并获取access_token。与getAccessToken不同的是会验证state值是否匹配,防止csrf攻击。
+     *
+     * @param string $storeState 存储的正确的state
+     * @param string $code       第一步里$redirectUri地址中传过来的code,为null则通过get参数获取
+     * @param string $state      回调接收到的state,为null则通过get参数获取
+     *
+     * @return string
+     */
+    protected function __getAccessToken($storeState, $code = null, $state = null)
+    {
+        $this->result = $this->http->get($this->getUrl('api/oauth/access_token'), [
+            'client_id'		    => $this->appid,
+            'client_secret'	 => $this->appSecret,
+            'grant_type'	    => 'authorization_code',
+            'code'			        => isset($code) ? $code : (isset($_GET['code']) ? $_GET['code'] : ''),
+        ])->json(true);
+        if ($this->isSuccess($this->result))
+        {
+            return $this->accessToken = $this->result['access_token'];
+        }
+        else
+        {
+            throw new ApiException($this->getErrorCode($this->result), $this->getErrorMsg($this->result));
+        }
+    }
+
+    /**
+     * 获取用户资料.
+     *
+     * @param string $accessToken
+     *
+     * @return array
+     */
+    public function getUserInfo($accessToken = null)
+    {
+        $this->result = $this->http->get($this->getUrl('api/account/current_user', [
+            'access_token'	 => null === $accessToken ? $this->accessToken : $accessToken,
+        ]))->json(true);
+        if ($this->isSuccess($this->result))
+        {
+            $this->openid = $this->result['data']['global_key'];
+
+            return $this->result;
+        }
+        else
+        {
+            throw new ApiException($this->getErrorCode($this->result), $this->getErrorMsg($this->result));
+        }
+    }
+
+    /**
+     * 刷新AccessToken续期
+     *
+     * @param string $refreshToken
+     *
+     * @return bool
+     */
+    public function refreshToken($refreshToken)
+    {
+        // api/oauth/access_token接口返回了refresh_token,但没刷新接口
+        return false;
+    }
+
+    /**
+     * 检验授权凭证AccessToken是否有效.
+     *
+     * @param string $accessToken
+     *
+     * @return bool
+     */
+    public function validateAccessToken($accessToken = null)
+    {
+        try
+        {
+            $this->getUserInfo($accessToken);
+
+            return true;
+        }
+        catch (ApiException $e)
+        {
+            return false;
+        }
+    }
+
+    public function isSuccess($result)
+    {
+        return !isset($result['code']) || 0 == $result['code'];
+    }
+
+    public function getErrorCode($result)
+    {
+        if (isset($result['msg']))
+        {
+            $keys = array_keys($result['msg']);
+
+            return isset($keys[0]) ? $keys[0] : '';
+        }
+    }
+
+    public function getErrorMsg($result)
+    {
+        if (isset($result['msg']))
+        {
+            $values = array_values($result['msg']);
+
+            return isset($values[0]) ? $values[0] : '';
+        }
+    }
+}

+ 84 - 0
vendor/yurunsoft/yurun-oauth-login/src/Coding/loginAgent.html

@@ -0,0 +1,84 @@
+<!DOCTYPE html>
+<html>
+
+    <head>
+        <meta charset="UTF-8">
+        <title>Coding登录</title>
+    </head>
+
+    <body>
+        <script>
+            var GWC = {
+                version: '1.0.0',
+                urlParams: {},
+                appendParams: function(url, params) {
+                    if (params) {
+                        var baseWithSearch = url.split('#')[0];
+                        var hash = url.split('#')[1];
+                        for (var key in params) {
+                            var attrValue = params[key];
+                            if (attrValue !== undefined) {
+                                var newParam = key + "=" + attrValue;
+                                if (baseWithSearch.indexOf('?') > 0) {
+                                    var oldParamReg = new RegExp('^' + key + '=[-%.!~*\'\(\)\\w]*', 'g');
+                                    if (oldParamReg.test(baseWithSearch)) {
+                                        baseWithSearch = baseWithSearch.replace(oldParamReg, newParam);
+                                    } else {
+                                        baseWithSearch += "&" + newParam;
+                                    }
+                                } else {
+                                    baseWithSearch += "?" + newParam;
+                                }
+                            }
+                        }
+
+                        if (hash) {
+                            url = baseWithSearch + '#' + hash;
+                        } else {
+                            url = baseWithSearch;
+                        }
+                    }
+                    return url;
+                },
+                getUrlParams: function() {
+                    var pairs = location.search.substring(1).split('&');
+                    for (var i = 0; i < pairs.length; i++) {
+                        var pos = pairs[i].indexOf('=');
+                        if (pos === -1) {
+                            continue;
+                        }
+                        GWC.urlParams[pairs[i].substring(0, pos)] = decodeURIComponent(pairs[i].substring(pos + 1));
+                    }
+                },
+                doRedirect: function() {
+					var code = GWC.urlParams['code'];
+                    var appId = GWC.urlParams['client_id'];
+                    var scope = GWC.urlParams['scope'];
+                    var response_type = GWC.urlParams['response_type'];
+                    var redirectUri;
+
+                    if (!code) {
+                        //第一步,没有拿到code,跳转至授权页面获取code
+                        redirectUri = GWC.appendParams('https://coding.net/oauth_authorize.html', {
+                            'client_id': appId,
+                            'redirect_uri': encodeURIComponent(location.href),
+                            'response_type': response_type,
+                            'scope': scope,
+                        });
+                    } else {
+                        //第二步,从授权页面跳转回来,已经获取到了code,再次跳转到实际所需页面
+                        redirectUri = GWC.appendParams(GWC.urlParams['redirect_uri'], {
+                            'code': code,
+                        });
+                    }
+
+                    location.href = redirectUri;
+                }
+            };
+
+            GWC.getUrlParams();
+            GWC.doRedirect();
+        </script>
+    </body>
+
+</html>

+ 172 - 0
vendor/yurunsoft/yurun-oauth-login/src/Gitee/OAuth2.php

@@ -0,0 +1,172 @@
+<?php
+
+namespace Yurun\OAuthLogin\Gitee;
+
+use Yurun\OAuthLogin\ApiException;
+use Yurun\OAuthLogin\Base;
+
+class OAuth2 extends Base
+{
+    /**
+     * api域名.
+     */
+    const API_DOMAIN = 'https://gitee.com/';
+
+    /**
+     * 获取url地址
+     *
+     * @param string $name   跟在域名后的文本
+     * @param array  $params GET参数
+     *
+     * @return string
+     */
+    public function getUrl($name, $params = [])
+    {
+        return static::API_DOMAIN . $name . (empty($params) ? '' : ('?' . $this->http_build_query($params)));
+    }
+
+    /**
+     * 使用账号密码方式登录授权.
+     *
+     * @param string $username 用户名
+     * @param string $password 密码
+     * @param string $scope    请求用户授权时向用户显示的可进行授权的列表,多个用空格分隔
+     *
+     * @return void
+     */
+    public function login($username, $password, $scope = 'user_info')
+    {
+        $response = $this->http->post($this->getUrl('oauth/token'), [
+            'grant_type'	    => 'password',
+            'username'		     => $username,
+            'password'		     => $password,
+            'client_id'		    => $this->appid,
+            'client_secret'	 => $this->appSecret,
+            'scope'			       => $scope,
+        ]);
+        $this->result = $response->json(true);
+        if (!isset($this->result['error']))
+        {
+            return $this->accessToken = $this->result['access_token'];
+        }
+        else
+        {
+            throw new ApiException(isset($this->result['error_description']) ? $this->result['error_description'] : '', $response->httpCode());
+        }
+    }
+
+    /**
+     * 第一步:获取登录页面跳转url.
+     *
+     * @param string $callbackUrl 登录回调地址
+     * @param string $state       状态值,不传则自动生成,随后可以通过->state获取。用于第三方应用防止CSRF攻击,成功授权后回调时会原样带回。一般为每个用户登录时随机生成state存在session中,登录回调中判断state是否和session中相同
+     * @param array  $scope       无用
+     *
+     * @return string
+     */
+    public function getAuthUrl($callbackUrl = null, $state = null, $scope = null)
+    {
+        $option = [
+            'client_id'			    => $this->appid,
+            'redirect_uri'		  => null === $callbackUrl ? $this->callbackUrl : $callbackUrl,
+            'response_type'		 => 'code',
+            'state'				       => $this->getState($state),
+        ];
+        if (null === $this->loginAgentUrl)
+        {
+            return $this->getUrl('oauth/authorize', $option);
+        }
+        else
+        {
+            return $this->loginAgentUrl . '?' . $this->http_build_query($option);
+        }
+    }
+
+    /**
+     * 第二步:处理回调并获取access_token。与getAccessToken不同的是会验证state值是否匹配,防止csrf攻击。
+     *
+     * @param string $storeState 存储的正确的state
+     * @param string $code       第一步里$redirectUri地址中传过来的code,为null则通过get参数获取
+     * @param string $state      回调接收到的state,为null则通过get参数获取
+     *
+     * @return string
+     */
+    protected function __getAccessToken($storeState, $code = null, $state = null)
+    {
+        $response = $this->http->post($this->getUrl('oauth/token'), [
+            'grant_type'	    => 'authorization_code',
+            'code'			        => isset($code) ? $code : (isset($_GET['code']) ? $_GET['code'] : ''),
+            'client_id'		    => $this->appid,
+            'redirect_uri'	  => $this->getRedirectUri(),
+            'client_secret'	 => $this->appSecret,
+        ]);
+        $this->result = $response->json(true);
+        if (!isset($this->result['error']))
+        {
+            return $this->accessToken = $this->result['access_token'];
+        }
+        else
+        {
+            throw new ApiException(isset($this->result['error_description']) ? $this->result['error_description'] : '', $response->httpCode());
+        }
+    }
+
+    /**
+     * 获取用户资料.
+     *
+     * @param string $accessToken
+     *
+     * @return array
+     */
+    public function getUserInfo($accessToken = null)
+    {
+        $response = $this->http->get($this->getUrl('api/v5/user', [
+            'access_token'	 => null === $accessToken ? $this->accessToken : $accessToken,
+        ]));
+        $this->result = $response->json(true);
+        if (isset($this->result['id']))
+        {
+            $this->openid = $this->result['id'];
+
+            return $this->result;
+        }
+        else
+        {
+            throw new ApiException(isset($this->result['message']) ? $this->result['message'] : '', $response->httpCode());
+        }
+    }
+
+    /**
+     * 刷新AccessToken续期
+     *
+     * @param string $refreshToken
+     *
+     * @return bool
+     */
+    public function refreshToken($refreshToken)
+    {
+        // 不支持
+        return false;
+    }
+
+    /**
+     * 检验授权凭证AccessToken是否有效.
+     *
+     * @param string $accessToken
+     *
+     * @return bool
+     */
+    public function validateAccessToken($accessToken = null)
+    {
+        try
+        {
+            $this->getUserInfo($accessToken);
+
+            return true;
+        }
+        catch (ApiException $e)
+        {
+            return false;
+        }
+    }
+}

+ 87 - 0
vendor/yurunsoft/yurun-oauth-login/src/Gitee/loginAgent.html

@@ -0,0 +1,87 @@
+<!DOCTYPE html>
+<html>
+
+    <head>
+        <meta charset="UTF-8">
+        <title>Gitee登录</title>
+    </head>
+
+    <body>
+        <script>
+            var GWC = {
+                version: '1.0.0',
+                urlParams: {},
+                appendParams: function(url, params) {
+                    if (params) {
+                        var baseWithSearch = url.split('#')[0];
+                        var hash = url.split('#')[1];
+                        for (var key in params) {
+                            var attrValue = params[key];
+                            if (attrValue !== undefined) {
+                                var newParam = key + "=" + attrValue;
+                                if (baseWithSearch.indexOf('?') > 0) {
+                                    var oldParamReg = new RegExp('^' + key + '=[-%.!~*\'\(\)\\w]*', 'g');
+                                    if (oldParamReg.test(baseWithSearch)) {
+                                        baseWithSearch = baseWithSearch.replace(oldParamReg, newParam);
+                                    } else {
+                                        baseWithSearch += "&" + newParam;
+                                    }
+                                } else {
+                                    baseWithSearch += "?" + newParam;
+                                }
+                            }
+                        }
+
+                        if (hash) {
+                            url = baseWithSearch + '#' + hash;
+                        } else {
+                            url = baseWithSearch;
+                        }
+                    }
+                    return url;
+                },
+                getUrlParams: function() {
+                    var pairs = location.search.substring(1).split('&');
+                    for (var i = 0; i < pairs.length; i++) {
+                        var pos = pairs[i].indexOf('=');
+                        if (pos === -1) {
+                            continue;
+                        }
+                        GWC.urlParams[pairs[i].substring(0, pos)] = decodeURIComponent(pairs[i].substring(pos + 1));
+                    }
+                },
+                doRedirect: function() {
+					var code = GWC.urlParams['code'];
+                    var appId = GWC.urlParams['client_id'];
+                    var state = GWC.urlParams['state'];
+                    var response_type = GWC.urlParams['response_type'];
+                    var redirectUri;
+
+                    if (!code) {
+                        //第一步,没有拿到code,跳转至授权页面获取code
+                        redirectUri = GWC.appendParams('https://gitee.com/oauth/authorize', {
+                            'client_id': appId,
+                            'redirect_uri': encodeURIComponent(GWC.appendParams(location.href.split('?')[0], {
+                                'redirect_uri': encodeURIComponent(GWC.urlParams['redirect_uri']),
+                            })),
+                            'state': state,
+                            'response_type': response_type,
+                        });
+                    } else {
+                        //第二步,从授权页面跳转回来,已经获取到了code,再次跳转到实际所需页面
+                        redirectUri = GWC.appendParams(GWC.urlParams['redirect_uri'], {
+                            'code': code,
+                            'state': state
+                        });
+                    }
+
+                    location.href = redirectUri;
+                }
+            };
+
+            GWC.getUrlParams();
+            GWC.doRedirect();
+        </script>
+    </body>
+
+</html>

+ 166 - 0
vendor/yurunsoft/yurun-oauth-login/src/Github/OAuth2.php

@@ -0,0 +1,166 @@
+<?php
+
+namespace Yurun\OAuthLogin\Github;
+
+use Yurun\OAuthLogin\ApiException;
+use Yurun\OAuthLogin\Base;
+
+class OAuth2 extends Base
+{
+    /**
+     * 授权接口域名.
+     */
+    const AUTH_DOMAIN = 'https://github.com/';
+
+    /**
+     * api接口域名.
+     */
+    const API_DOMAIN = 'https://api.github.com/';
+
+    /**
+     * 是否在登录页显示注册,默认false.
+     *
+     * @var bool
+     */
+    public $allowSignup = false;
+
+    /**
+     * 获取登录授权url地址
+     *
+     * @param string $name   跟在域名后的文本
+     * @param array  $params GET参数
+     *
+     * @return string
+     */
+    public function getAuthLoginUrl($name, $params = [])
+    {
+        return static::AUTH_DOMAIN . $name . (empty($params) ? '' : ('?' . $this->http_build_query($params)));
+    }
+
+    /**
+     * 获取url地址
+     *
+     * @param string $name   跟在域名后的文本
+     * @param array  $params GET参数
+     *
+     * @return string
+     */
+    public function getUrl($name, $params = [])
+    {
+        return static::API_DOMAIN . $name . (empty($params) ? '' : ('?' . $this->http_build_query($params)));
+    }
+
+    /**
+     * 第一步:获取登录页面跳转url.
+     *
+     * @param string $callbackUrl 登录回调地址
+     * @param string $state       状态值,不传则自动生成,随后可以通过->state获取。用于第三方应用防止CSRF攻击,成功授权后回调时会原样带回。一般为每个用户登录时随机生成state存在session中,登录回调中判断state是否和session中相同
+     * @param array  $scope       请求用户授权时向用户显示的可进行授权的列表。可空
+     *
+     * @return string
+     */
+    public function getAuthUrl($callbackUrl = null, $state = null, $scope = null)
+    {
+        $option = [
+            'client_id'			   => $this->appid,
+            'redirect_uri'		 => null === $callbackUrl ? $this->callbackUrl : $callbackUrl,
+            'scope'				      => null === $scope ? $this->scope : $scope,
+            'state'				      => $this->getState($state),
+            'allow_signup'		 => $this->allowSignup,
+        ];
+        if (null === $this->loginAgentUrl)
+        {
+            return $this->getAuthLoginUrl('login/oauth/authorize', $option);
+        }
+        else
+        {
+            return $this->loginAgentUrl . '?' . $this->http_build_query($option);
+        }
+    }
+
+    /**
+     * 第二步:处理回调并获取access_token。与getAccessToken不同的是会验证state值是否匹配,防止csrf攻击。
+     *
+     * @param string $storeState 存储的正确的state
+     * @param string $code       第一步里$redirectUri地址中传过来的code,为null则通过get参数获取
+     * @param string $state      回调接收到的state,为null则通过get参数获取
+     *
+     * @return string
+     */
+    protected function __getAccessToken($storeState, $code = null, $state = null)
+    {
+        $this->result = $this->http->accept('application/json')->get($this->getAuthLoginUrl('login/oauth/access_token', [
+            'client_id'			    => $this->appid,
+            'client_secret'		 => $this->appSecret,
+            'code'				        => isset($code) ? $code : (isset($_GET['code']) ? $_GET['code'] : ''),
+            'redirect_uri'		  => $this->getRedirectUri(),
+            'state'				       => isset($state) ? $state : (isset($_GET['state']) ? $_GET['state'] : ''),
+        ]))->json(true);
+        if (isset($this->result['error']))
+        {
+            throw new ApiException($this->result['error'], 0);
+        }
+        else
+        {
+            return $this->accessToken = $this->result['access_token'];
+        }
+    }
+
+    /**
+     * 获取用户资料.
+     *
+     * @param string $accessToken
+     *
+     * @return array
+     */
+    public function getUserInfo($accessToken = null)
+    {
+        $this->result = $this->http->ua('YurunOAuthLogin')->get($this->getUrl('user', [
+            'access_token'			 => null === $accessToken ? $this->accessToken : $accessToken,
+        ]))->json(true);
+        if (isset($this->result['message']))
+        {
+            throw new ApiException($this->result['message'], 0);
+        }
+        else
+        {
+            $this->openid = $this->result['id'];
+
+            return $this->result;
+        }
+    }
+
+    /**
+     * 刷新AccessToken续期
+     *
+     * @param string $refreshToken
+     *
+     * @return bool
+     */
+    public function refreshToken($refreshToken)
+    {
+        // 不支持
+        return false;
+    }
+
+    /**
+     * 检验授权凭证AccessToken是否有效.
+     *
+     * @param string $accessToken
+     *
+     * @return bool
+     */
+    public function validateAccessToken($accessToken = null)
+    {
+        try
+        {
+            $this->getUserInfo($accessToken);
+
+            return true;
+        }
+        catch (ApiException $e)
+        {
+            return false;
+        }
+    }
+}

+ 87 - 0
vendor/yurunsoft/yurun-oauth-login/src/Github/loginAgent.html

@@ -0,0 +1,87 @@
+<!DOCTYPE html>
+<html>
+
+    <head>
+        <meta charset="UTF-8">
+        <title>Github登录</title>
+    </head>
+
+    <body>
+        <script>
+            var GWC = {
+                version: '1.0.0',
+                urlParams: {},
+                appendParams: function(url, params) {
+                    if (params) {
+                        var baseWithSearch = url.split('#')[0];
+                        var hash = url.split('#')[1];
+                        for (var key in params) {
+                            var attrValue = params[key];
+                            if (attrValue !== undefined) {
+                                var newParam = key + "=" + attrValue;
+                                if (baseWithSearch.indexOf('?') > 0) {
+                                    var oldParamReg = new RegExp('^' + key + '=[-%.!~*\'\(\)\\w]*', 'g');
+                                    if (oldParamReg.test(baseWithSearch)) {
+                                        baseWithSearch = baseWithSearch.replace(oldParamReg, newParam);
+                                    } else {
+                                        baseWithSearch += "&" + newParam;
+                                    }
+                                } else {
+                                    baseWithSearch += "?" + newParam;
+                                }
+                            }
+                        }
+
+                        if (hash) {
+                            url = baseWithSearch + '#' + hash;
+                        } else {
+                            url = baseWithSearch;
+                        }
+                    }
+                    return url;
+                },
+                getUrlParams: function() {
+                    var pairs = location.search.substring(1).split('&');
+                    for (var i = 0; i < pairs.length; i++) {
+                        var pos = pairs[i].indexOf('=');
+                        if (pos === -1) {
+                            continue;
+                        }
+                        GWC.urlParams[pairs[i].substring(0, pos)] = decodeURIComponent(pairs[i].substring(pos + 1));
+                    }
+                },
+                doRedirect: function() {
+					var code = GWC.urlParams['code'];
+                    var appId = GWC.urlParams['client_id'];
+                    var scope = GWC.urlParams['scope'];
+                    var state = GWC.urlParams['state'];
+                    var allow_signup = GWC.urlParams['allow_signup'];
+                    var redirectUri;
+
+                    if (!code) {
+                        //第一步,没有拿到code,跳转至授权页面获取code
+                        redirectUri = GWC.appendParams('https://github.com/login/oauth/authorize', {
+                            'client_id': appId,
+                            'redirect_uri': encodeURIComponent(location.href),
+                            'scope': scope,
+                            'state': state,
+                            'allow_signup': allow_signup,
+                        });
+                    } else {
+                        //第二步,从授权页面跳转回来,已经获取到了code,再次跳转到实际所需页面
+                        redirectUri = GWC.appendParams(GWC.urlParams['redirect_uri'], {
+                            'code': code,
+                            'state': state
+                        });
+                    }
+
+                    location.href = redirectUri;
+                }
+            };
+
+            GWC.getUrlParams();
+            GWC.doRedirect();
+        </script>
+    </body>
+
+</html>

+ 11 - 0
vendor/yurunsoft/yurun-oauth-login/src/Lib/Base.php

@@ -0,0 +1,11 @@
+<?php
+
+namespace Yurun\OAuthLogin\Lib;
+
+abstract class Base
+{
+    public static function parseKey($key)
+    {
+        return wordwrap(preg_replace('/[\r\n]/', '', $key), 64, "\n", true);
+    }
+}

+ 88 - 0
vendor/yurunsoft/yurun-oauth-login/src/Lib/RSA.php

@@ -0,0 +1,88 @@
+<?php
+
+namespace Yurun\OAuthLogin\Lib;
+
+class RSA extends Base
+{
+    public static function signPrivate($data, $key)
+    {
+        $key = static::parseKey($key);
+        $key = "-----BEGIN RSA PRIVATE KEY-----\n{$key}\n-----END RSA PRIVATE KEY-----";
+        openssl_sign($data, $sign, $key, \OPENSSL_ALGO_SHA1);
+
+        return $sign;
+    }
+
+    public static function signPrivateFromFile($data, $fileName)
+    {
+        $key = file_get_contents($fileName);
+        $res = openssl_get_privatekey($key);
+        if (!$res)
+        {
+            throw new \Exception('私钥文件格式错误');
+        }
+        openssl_sign($data, $sign, $res, \OPENSSL_ALGO_SHA1);
+        openssl_free_key($res);
+
+        return $sign;
+    }
+
+    public static function verifyPublic($data, $key, $sign)
+    {
+        $key = static::parseKey($key);
+        $key = "-----BEGIN PUBLIC KEY-----\n{$key}\n-----END PUBLIC KEY-----";
+
+        return 1 === openssl_verify($data, $sign, $key, \OPENSSL_ALGO_SHA1);
+    }
+
+    public static function verifyPublicFromFile($data, $fileName, $sign)
+    {
+        $key = file_get_contents($fileName);
+        $res = openssl_get_publickey($key);
+        if (!$res)
+        {
+            throw new \Exception('公钥文件格式错误');
+        }
+        $result = openssl_verify($data, $sign, $res, \OPENSSL_ALGO_SHA1);
+        openssl_free_key($res);
+
+        return 1 === $result;
+    }
+
+    public static function encryptPublicFromFile($data, $fileName)
+    {
+        $res = openssl_get_publickey(file_get_contents($fileName));
+        if (!$res)
+        {
+            throw new \Exception('公钥文件格式错误');
+        }
+        openssl_public_encrypt($data, $result, $res, \OPENSSL_PKCS1_OAEP_PADDING);
+        openssl_free_key($res);
+
+        return $result;
+    }
+
+    public static function encryptPublic($data, $public)
+    {
+        openssl_public_encrypt($data, $result, $public, \OPENSSL_PKCS1_OAEP_PADDING);
+
+        return $result;
+    }
+
+    /**
+     * pkcs1 格式转 pkcs8.
+     *
+     * @param string $srcFile
+     * @param string $destFile
+     *
+     * @return void
+     */
+    public static function pkcs1To8($srcFile, $destFile)
+    {
+        $content = exec("openssl rsa -RSAPublicKey_in -in {$srcFile} -pubout -out {$destFile}", $output, $code);
+        if (0 != $code)
+        {
+            throw new \RuntimeException(sprintf('Convert PKCS1 To PKCS8 failed! code:%s message:%s', $code, $content));
+        }
+    }
+}

+ 67 - 0
vendor/yurunsoft/yurun-oauth-login/src/Lib/RSA2.php

@@ -0,0 +1,67 @@
+<?php
+
+namespace Yurun\OAuthLogin\Lib;
+
+class RSA2 extends Base
+{
+    public static function signPrivate($data, $key)
+    {
+        if (!\defined('OPENSSL_ALGO_SHA256'))
+        {
+            throw new \Exception('SHA256需要在PHP>=5.4.8下才可使用');
+        }
+        $key = static::parseKey($key);
+        $key = "-----BEGIN RSA PRIVATE KEY-----\n{$key}\n-----END RSA PRIVATE KEY-----";
+        openssl_sign($data, $sign, $key, \OPENSSL_ALGO_SHA256);
+
+        return $sign;
+    }
+
+    public static function signPrivateFromFile($data, $fileName)
+    {
+        if (!\defined('OPENSSL_ALGO_SHA256'))
+        {
+            throw new \Exception('SHA256需要在PHP>=5.4.8下才可使用');
+        }
+        $key = file_get_contents($fileName);
+        $res = openssl_get_privatekey($key);
+        if (!$res)
+        {
+            throw new \Exception('私钥文件格式错误');
+        }
+        openssl_sign($data, $sign, $res, \OPENSSL_ALGO_SHA256);
+        openssl_free_key($res);
+
+        return $sign;
+    }
+
+    public static function verifyPublic($data, $key, $sign)
+    {
+        if (!\defined('OPENSSL_ALGO_SHA256'))
+        {
+            throw new \Exception('SHA256需要在PHP>=5.4.8下才可使用');
+        }
+        $key = static::parseKey($key);
+        $key = "-----BEGIN PUBLIC KEY-----\n{$key}\n-----END PUBLIC KEY-----";
+
+        return 1 === openssl_verify($data, $sign, $key, \OPENSSL_ALGO_SHA256);
+    }
+
+    public static function verifyPublicFromFile($data, $fileName, $sign)
+    {
+        if (!\defined('OPENSSL_ALGO_SHA256'))
+        {
+            throw new \Exception('SHA256需要在PHP>=5.4.8下才可使用');
+        }
+        $key = file_get_contents($fileName);
+        $res = openssl_get_publickey($key);
+        if (!$res)
+        {
+            throw new \Exception('公钥文件格式错误');
+        }
+        $result = openssl_verify($data, $sign, $res, \OPENSSL_ALGO_SHA256);
+        openssl_free_key($res);
+
+        return 1 === $result;
+    }
+}

+ 159 - 0
vendor/yurunsoft/yurun-oauth-login/src/OSChina/OAuth2.php

@@ -0,0 +1,159 @@
+<?php
+
+namespace Yurun\OAuthLogin\OSChina;
+
+use Yurun\OAuthLogin\ApiException;
+use Yurun\OAuthLogin\Base;
+
+class OAuth2 extends Base
+{
+    /**
+     * api域名.
+     */
+    const API_DOMAIN = 'https://www.oschina.net/';
+
+    /**
+     * 获取url地址
+     *
+     * @param string $name   跟在域名后的文本
+     * @param array  $params GET参数
+     *
+     * @return string
+     */
+    public function getUrl($name, $params = [])
+    {
+        return static::API_DOMAIN . $name . (empty($params) ? '' : ('?' . $this->http_build_query($params)));
+    }
+
+    /**
+     * 第一步:获取登录页面跳转url.
+     *
+     * @param string $callbackUrl 登录回调地址
+     * @param string $state       状态值,不传则自动生成,随后可以通过->state获取。用于第三方应用防止CSRF攻击,成功授权后回调时会原样带回。一般为每个用户登录时随机生成state存在session中,登录回调中判断state是否和session中相同
+     * @param array  $scope       无用
+     *
+     * @return string
+     */
+    public function getAuthUrl($callbackUrl = null, $state = null, $scope = null)
+    {
+        $option = [
+            'client_id'			    => $this->appid,
+            'response_type'		 => 'code',
+            'redirect_uri'		  => null === $callbackUrl ? $this->callbackUrl : $callbackUrl,
+            'state'				       => $this->getState($state),
+        ];
+        if (null === $this->loginAgentUrl)
+        {
+            return $this->getUrl('action/oauth2/authorize', $option);
+        }
+        else
+        {
+            return $this->loginAgentUrl . '?' . $this->http_build_query($option);
+        }
+    }
+
+    /**
+     * 第二步:处理回调并获取access_token。与getAccessToken不同的是会验证state值是否匹配,防止csrf攻击。
+     *
+     * @param string $storeState 存储的正确的state
+     * @param string $code       第一步里$redirectUri地址中传过来的code,为null则通过get参数获取
+     * @param string $state      回调接收到的state,为null则通过get参数获取
+     *
+     * @return string
+     */
+    protected function __getAccessToken($storeState, $code = null, $state = null)
+    {
+        $response = $this->http->get($this->getUrl('action/openapi/token'), [
+            'client_id'		    => $this->appid,
+            'client_secret'	 => $this->appSecret,
+            'grant_type'	    => 'authorization_code',
+            'redirect_uri'	  => $this->getRedirectUri(),
+            'code'			        => isset($code) ? $code : (isset($_GET['code']) ? $_GET['code'] : ''),
+            'dataType'		     => 'json',
+        ]);
+        $this->result = $response->json(true);
+        if (!isset($this->result['error']))
+        {
+            return $this->accessToken = $this->result['access_token'];
+        }
+        else
+        {
+            throw new ApiException(isset($this->result['error_description']) ? $this->result['error_description'] : '', $response->httpCode());
+        }
+    }
+
+    /**
+     * 获取用户资料.
+     *
+     * @param string $accessToken
+     *
+     * @return array
+     */
+    public function getUserInfo($accessToken = null)
+    {
+        $response = $this->http->get($this->getUrl('action/openapi/user', [
+            'access_token'	 => null === $accessToken ? $this->accessToken : $accessToken,
+            'dataType'		    => 'json',
+        ]));
+        $this->result = $response->json(true);
+        if (isset($this->result['id']))
+        {
+            $this->openid = $this->result['id'];
+
+            return $this->result;
+        }
+        else
+        {
+            throw new ApiException(isset($this->result['error_description']) ? $this->result['error_description'] : '', $response->httpCode());
+        }
+    }
+
+    /**
+     * 刷新AccessToken续期
+     *
+     * @param string $refreshToken
+     *
+     * @return bool
+     */
+    public function refreshToken($refreshToken)
+    {
+        $response = $this->http->get($this->getUrl('action/openapi/token'), [
+            'client_id'		    => $this->appid,
+            'client_secret'	 => $this->appSecret,
+            'grant_type'	    => 'refresh_token',
+            'redirect_uri'	  => $this->getRedirectUri(),
+            'refresh_token'	 => $refreshToken,
+            'dataType'		     => 'json',
+        ]);
+        $this->result = $response->json(true);
+        if (!isset($this->result['error']))
+        {
+            return $this->accessToken = $this->result['access_token'];
+        }
+        else
+        {
+            throw new ApiException(isset($this->result['error_description']) ? $this->result['error_description'] : '', $response->httpCode());
+        }
+    }
+
+    /**
+     * 检验授权凭证AccessToken是否有效.
+     *
+     * @param string $accessToken
+     *
+     * @return bool
+     */
+    public function validateAccessToken($accessToken = null)
+    {
+        try
+        {
+            $this->getUserInfo($accessToken);
+
+            return true;
+        }
+        catch (ApiException $e)
+        {
+            return false;
+        }
+    }
+}

+ 86 - 0
vendor/yurunsoft/yurun-oauth-login/src/OSChina/loginAgent.html

@@ -0,0 +1,86 @@
+<!DOCTYPE html>
+<html>
+
+    <head>
+        <meta charset="UTF-8">
+        <title>OSChina登录</title>
+    </head>
+
+    <body>
+        <script>
+            var GWC = {
+                version: '1.0.0',
+                urlParams: {},
+                appendParams: function(url, params) {
+                    if (params) {
+                        var baseWithSearch = url.split('#')[0];
+                        var hash = url.split('#')[1];
+                        for (var key in params) {
+                            var attrValue = params[key];
+                            if (attrValue !== undefined) {
+                                var newParam = key + "=" + attrValue;
+                                if (baseWithSearch.indexOf('?') > 0) {
+                                    var oldParamReg = new RegExp('^' + key + '=[-%.!~*\'\(\)\\w]*', 'g');
+                                    if (oldParamReg.test(baseWithSearch)) {
+                                        baseWithSearch = baseWithSearch.replace(oldParamReg, newParam);
+                                    } else {
+                                        baseWithSearch += "&" + newParam;
+                                    }
+                                } else {
+                                    baseWithSearch += "?" + newParam;
+                                }
+                            }
+                        }
+
+                        if (hash) {
+                            url = baseWithSearch + '#' + hash;
+                        } else {
+                            url = baseWithSearch;
+                        }
+                    }
+                    return url;
+                },
+                getUrlParams: function() {
+                    var pairs = location.search.substring(1).split('&');
+                    for (var i = 0; i < pairs.length; i++) {
+                        var pos = pairs[i].indexOf('=');
+                        if (pos === -1) {
+                            continue;
+                        }
+                        GWC.urlParams[pairs[i].substring(0, pos)] = decodeURIComponent(pairs[i].substring(pos + 1));
+                    }
+                },
+                doRedirect: function() {
+					var code = GWC.urlParams['code'];
+                    var appId = GWC.urlParams['client_id'];
+                    var state = GWC.urlParams['state'];
+                    var response_type = GWC.urlParams['response_type'];
+                    var redirectUri;
+
+                    if (!code) {
+                        //第一步,没有拿到code,跳转至授权页面获取code
+                        redirectUri = GWC.appendParams('http://www.oschina.net/action/oauth2/authorize', {
+                            'client_id': appId,
+                            'response_type': response_type,
+                            'redirect_uri': encodeURIComponent(GWC.appendParams(location.href.split('?')[0], {
+                                'redirect_uri': encodeURIComponent(GWC.urlParams['redirect_uri']),
+                            })),
+                            'state': state,
+                        });
+                    } else {
+                        //第二步,从授权页面跳转回来,已经获取到了code,再次跳转到实际所需页面
+                        redirectUri = GWC.appendParams(GWC.urlParams['redirect_uri'], {
+                            'code': code,
+                            'state': state,
+                        });
+                    }
+                    location.href = redirectUri;
+                }
+            };
+
+            GWC.getUrlParams();
+            GWC.doRedirect();
+        </script>
+    </body>
+
+</html>

+ 290 - 0
vendor/yurunsoft/yurun-oauth-login/src/QQ/OAuth2.php

@@ -0,0 +1,290 @@
+<?php
+
+namespace Yurun\OAuthLogin\QQ;
+
+use Yurun\OAuthLogin\ApiException;
+use Yurun\OAuthLogin\Base;
+
+class OAuth2 extends Base
+{
+    /**
+     * api接口域名.
+     */
+    const API_DOMAIN = 'https://graph.qq.com/';
+
+    /**
+     * 仅PC网站接入时使用。用于展示的样式。不传则默认展示为PC下的样式。如果传入“mobile”,则展示为mobile端下的样式。
+     *
+     * @var string
+     */
+    public $display;
+
+    /**
+     * openid从哪个字段取,默认为openid.
+     *
+     * @var int
+     */
+    public $openidMode = OpenidMode::OPEN_ID;
+
+    /**
+     * 是否使用unionid,默认为false.
+     *
+     * @var bool
+     */
+    public $isUseUnionID = false;
+
+    /**
+     * 获取url地址
+     *
+     * @param string $name   跟在域名后的文本
+     * @param array  $params GET参数
+     *
+     * @return string
+     */
+    public function getUrl($name, $params = [])
+    {
+        return static::API_DOMAIN . $name . (empty($params) ? '' : ('?' . $this->http_build_query($params)));
+    }
+
+    /**
+     * 第一步:获取登录页面跳转url.
+     *
+     * @param string $callbackUrl 登录回调地址
+     * @param string $state       状态值,不传则自动生成,随后可以通过->state获取。用于第三方应用防止CSRF攻击,成功授权后回调时会原样带回。一般为每个用户登录时随机生成state存在session中,登录回调中判断state是否和session中相同
+     * @param array  $scope       请求用户授权时向用户显示的可进行授权的列表。可空
+     *
+     * @return string
+     */
+    public function getAuthUrl($callbackUrl = null, $state = null, $scope = null)
+    {
+        $option = [
+            'response_type'		 => 'code',
+            'client_id'			    => $this->appid,
+            'redirect_uri'		  => null === $callbackUrl ? $this->callbackUrl : $callbackUrl,
+            'state'				       => $this->getState($state),
+            'scope'				       => null === $scope ? $this->scope : $scope,
+            'display'			      => $this->display,
+        ];
+        if (null === $this->loginAgentUrl)
+        {
+            return $this->getUrl('oauth2.0/authorize', $option);
+        }
+        else
+        {
+            return $this->loginAgentUrl . '?' . $this->http_build_query($option);
+        }
+    }
+
+    /**
+     * 第二步:处理回调并获取access_token。与getAccessToken不同的是会验证state值是否匹配,防止csrf攻击。
+     *
+     * @param string $storeState 存储的正确的state
+     * @param string $code       第一步里$redirectUri地址中传过来的code,为null则通过get参数获取
+     * @param string $state      回调接收到的state,为null则通过get参数获取
+     *
+     * @return string
+     */
+    protected function __getAccessToken($storeState, $code = null, $state = null)
+    {
+        $content = $this->http->get($this->getUrl('oauth2.0/token', [
+            'grant_type'	    => 'authorization_code',
+            'client_id'		    => $this->appid,
+            'client_secret'	 => $this->appSecret,
+            'code'			        => isset($code) ? $code : (isset($_GET['code']) ? $_GET['code'] : ''),
+            'state'			       => isset($state) ? $state : (isset($_GET['state']) ? $_GET['state'] : ''),
+            'redirect_uri'	  => $this->getRedirectUri(),
+        ]))->body();
+        $jsonData = json_decode($content, true);
+        if ($jsonData)
+        {
+            $this->result = $jsonData;
+            throw new ApiException($jsonData['error_description'], $jsonData['error']);
+        }
+        parse_str($content, $result);
+        $this->result = $result;
+        if (isset($this->result['code']) && 0 != $this->result['code'])
+        {
+            throw new ApiException($this->result['msg'], $this->result['code']);
+        }
+        else
+        {
+            return $this->accessToken = $this->result['access_token'];
+        }
+    }
+
+    /**
+     * 获取用户资料.
+     *
+     * @param string $accessToken
+     *
+     * @return array
+     */
+    public function getUserInfo($accessToken = null)
+    {
+        if (null === $this->openid)
+        {
+            $this->getOpenID($accessToken);
+        }
+        $this->result = $this->http->get($this->getUrl('user/get_user_info', [
+            'access_token'			     => null === $accessToken ? $this->accessToken : $accessToken,
+            'oauth_consumer_key'	 => $this->appid,
+            'openid'				          => $this->openid,
+        ]))->json(true);
+        if (isset($this->result['ret']) && 0 != $this->result['ret'])
+        {
+            throw new ApiException($this->result['msg'], $this->result['ret']);
+        }
+        else
+        {
+            return $this->result;
+        }
+    }
+
+    /**
+     * 刷新AccessToken续期
+     *
+     * @param string $refreshToken
+     *
+     * @return bool
+     */
+    public function refreshToken($refreshToken)
+    {
+        $this->result = $this->http->get($this->getUrl('oauth2.0/token', [
+            'grant_type'	    => 'refresh_token',
+            'client_id'		    => $this->appid,
+            'client_secret'	 => $this->appSecret,
+            'refresh_token'	 => $refreshToken,
+        ]))->jsonp(true);
+
+        return isset($this->result['code']) && 0 == $this->result['code'];
+    }
+
+    /**
+     * 检验授权凭证AccessToken是否有效.
+     *
+     * @param string $accessToken
+     *
+     * @return bool
+     */
+    public function validateAccessToken($accessToken = null)
+    {
+        try
+        {
+            $this->getOpenID($accessToken);
+
+            return true;
+        }
+        catch (ApiException $e)
+        {
+            return false;
+        }
+    }
+
+    /**
+     * 获取OpenID.
+     *
+     * @param string $accessToken
+     *
+     * @return string
+     */
+    public function getOpenID($accessToken = null)
+    {
+        $params = [
+            'access_token'	 => null === $accessToken ? $this->accessToken : $accessToken,
+        ];
+        if ($this->isUseUnionID && OpenidMode::UNION_ID === $this->openidMode)
+        {
+            $params['unionid'] = $this->openidMode;
+        }
+        $this->result = $this->http->get($this->getUrl('oauth2.0/me', $params))->jsonp(true);
+        if (isset($this->result['error']))
+        {
+            throw new ApiException($this->result['error_description'], $this->result['error']);
+        }
+        else
+        {
+            $this->openid = $this->result['openid'];
+            if ($this->isUseUnionID && OpenidMode::UNION_ID === $this->openidMode)
+            {
+                return $this->result['unionid'];
+            }
+            else
+            {
+                return $this->openid;
+            }
+        }
+    }
+
+    /**
+     * QQ小程序登录凭证校验,获取session_key、openid、unionid
+     * 返回session_key
+     * 调用后可以使用$this->result['openid']或$this->result['unionid']获取相应的值
+     *
+     * @param string $jsCode
+     *
+     * @return string
+     */
+    public function getSessionKey($jsCode)
+    {
+        $this->result = $this->http->get('https://api.q.qq.com/sns/jscode2session', [
+            'appid'		    => $this->appid,
+            'secret'	    => $this->appSecret,
+            'js_code'	   => $jsCode,
+            'grant_type' => 'authorization_code',
+        ])->json(true);
+
+        if (isset($this->result['errcode']) && 0 != $this->result['errcode'])
+        {
+            throw new ApiException($this->result['errmsg'], $this->result['errcode']);
+        }
+        else
+        {
+            switch ((int) $this->openidMode)
+            {
+                case OpenidMode::OPEN_ID:
+                    $this->openid = $this->result['openid'];
+                    break;
+                case OpenidMode::UNION_ID:
+                    $this->openid = $this->result['unionid'];
+                    break;
+                case OpenidMode::UNION_ID_FIRST:
+                    $this->openid = empty($this->result['unionid']) ? $this->result['openid'] : $this->result['unionid'];
+                    break;
+            }
+        }
+
+        return $this->result['session_key'];
+    }
+
+    /**
+     * 解密小程序 qq.getUserInfo() 敏感数据.
+     *
+     * @param string $encryptedData
+     * @param string $iv
+     * @param string $sessionKey
+     *
+     * @return array
+     */
+    public function descryptData($encryptedData, $iv, $sessionKey)
+    {
+        if (24 != \strlen($sessionKey))
+        {
+            throw new \InvalidArgumentException('sessionKey 格式错误');
+        }
+        if (24 != \strlen($iv))
+        {
+            throw new \InvalidArgumentException('iv 格式错误');
+        }
+        $aesKey = base64_decode($sessionKey);
+        $aesIV = base64_decode($iv);
+        $aesCipher = base64_decode($encryptedData);
+        $result = openssl_decrypt($aesCipher, 'AES-128-CBC', $aesKey, 1, $aesIV);
+        $dataObj = json_decode($result, true);
+        if (!$dataObj)
+        {
+            throw new \InvalidArgumentException('反序列化数据失败');
+        }
+
+        return $dataObj;
+    }
+}

+ 16 - 0
vendor/yurunsoft/yurun-oauth-login/src/QQ/OpenidMode.php

@@ -0,0 +1,16 @@
+<?php
+
+namespace Yurun\OAuthLogin\QQ;
+
+class OpenidMode
+{
+    /**
+     * 使用openid.
+     */
+    const OPEN_ID = 1;
+
+    /**
+     * 使用unionid.
+     */
+    const UNION_ID = 2;
+}

+ 87 - 0
vendor/yurunsoft/yurun-oauth-login/src/QQ/loginAgent.html

@@ -0,0 +1,87 @@
+<!DOCTYPE html>
+<html>
+
+    <head>
+        <meta charset="UTF-8">
+        <title>QQ登录</title>
+    </head>
+
+    <body>
+        <script>
+            var GWC = {
+                version: '1.0.0',
+                urlParams: {},
+                appendParams: function(url, params) {
+                    if (params) {
+                        var baseWithSearch = url.split('#')[0];
+                        var hash = url.split('#')[1];
+                        for (var key in params) {
+                            var attrValue = params[key];
+                            if (attrValue !== undefined) {
+                                var newParam = key + "=" + attrValue;
+                                if (baseWithSearch.indexOf('?') > 0) {
+                                    var oldParamReg = new RegExp('^' + key + '=[-%.!~*\'\(\)\\w]*', 'g');
+                                    if (oldParamReg.test(baseWithSearch)) {
+                                        baseWithSearch = baseWithSearch.replace(oldParamReg, newParam);
+                                    } else {
+                                        baseWithSearch += "&" + newParam;
+                                    }
+                                } else {
+                                    baseWithSearch += "?" + newParam;
+                                }
+                            }
+                        }
+
+                        if (hash) {
+                            url = baseWithSearch + '#' + hash;
+                        } else {
+                            url = baseWithSearch;
+                        }
+                    }
+                    return url;
+                },
+                getUrlParams: function() {
+                    var pairs = location.search.substring(1).split('&');
+                    for (var i = 0; i < pairs.length; i++) {
+                        var pos = pairs[i].indexOf('=');
+                        if (pos === -1) {
+                            continue;
+                        }
+                        GWC.urlParams[pairs[i].substring(0, pos)] = decodeURIComponent(pairs[i].substring(pos + 1));
+                    }
+                },
+                doRedirect: function() {
+					var code = GWC.urlParams['code'];
+					var responseType = GWC.urlParams['response_type'];
+                    var appId = GWC.urlParams['client_id'];
+                    var scope = GWC.urlParams['scope'] || 'get_user_info';
+                    var state = GWC.urlParams['state'];
+                    var redirectUri;
+
+                    if (!code) {
+                        //第一步,没有拿到code,跳转至QQ授权页面获取code
+                        redirectUri = GWC.appendParams('https://graph.qq.com/oauth2.0/authorize', {
+							'response_type': responseType,
+                            'client_id': appId,
+                            'redirect_uri': encodeURIComponent(location.href),
+                            'scope': scope,
+                            'state': state,
+                        });
+                    } else {
+                        //第二步,从QQ授权页面跳转回来,已经获取到了code,再次跳转到实际所需页面
+                        redirectUri = GWC.appendParams(GWC.urlParams['redirect_uri'], {
+                            'code': code,
+                            'state': state
+                        });
+                    }
+
+                    location.href = redirectUri;
+                }
+            };
+
+            GWC.getUrlParams();
+            GWC.doRedirect();
+        </script>
+    </body>
+
+</html>

+ 199 - 0
vendor/yurunsoft/yurun-oauth-login/src/Weibo/OAuth2.php

@@ -0,0 +1,199 @@
+<?php
+
+namespace Yurun\OAuthLogin\Weibo;
+
+use Yurun\OAuthLogin\ApiException;
+use Yurun\OAuthLogin\Base;
+
+class OAuth2 extends Base
+{
+    /**
+     * api域名.
+     */
+    const API_DOMAIN = 'https://api.weibo.com/';
+
+    /**
+     * 当display=mobile时,使用该域名.
+     */
+    const API_MOBILE_DOMAIN = 'https://open.weibo.cn/';
+
+    /**
+     * 授权页面的终端类型,取值见微博文档。http://open.weibo.com/wiki/Oauth2/authorize.
+     *
+     * @var string
+     */
+    public $display;
+
+    /**
+     * 是否强制用户重新登录,true:是,false:否。默认false。
+     *
+     * @var bool
+     */
+    public $forcelogin = false;
+
+    /**
+     * 授权页语言,缺省为中文简体版,en为英文版。
+     *
+     * @var string
+     */
+    public $language;
+
+    /**
+     * 获取用户资料时传的参数,可空.
+     *
+     * @var string
+     */
+    public $screenName;
+
+    /**
+     * 获取url地址
+     *
+     * @param string $name   跟在域名后的文本
+     * @param array  $params GET参数
+     *
+     * @return string
+     */
+    public function getUrl($name, $params = [])
+    {
+        return static::API_DOMAIN . $name . (empty($params) ? '' : ('?' . $this->http_build_query($params)));
+    }
+
+    /**
+     * 获取display=mobile时的url地址
+     *
+     * @param string $name   跟在域名后的文本
+     * @param array  $params GET参数
+     *
+     * @return string
+     */
+    public function getMobileUrl($name, $params)
+    {
+        return static::API_MOBILE_DOMAIN . $name . (empty($params) ? '' : ('?' . $this->http_build_query($params)));
+    }
+
+    /**
+     * 第一步:获取登录页面跳转url.
+     *
+     * @param string $callbackUrl 登录回调地址
+     * @param string $state       状态值,不传则自动生成,随后可以通过->state获取。用于第三方应用防止CSRF攻击,成功授权后回调时会原样带回。一般为每个用户登录时随机生成state存在session中,登录回调中判断state是否和session中相同
+     * @param array  $scope       请求用户授权时向用户显示的可进行授权的列表。可空
+     *
+     * @return string
+     */
+    public function getAuthUrl($callbackUrl = null, $state = null, $scope = null)
+    {
+        $option = [
+            'client_id'			   => $this->appid,
+            'redirect_uri'		 => null === $callbackUrl ? $this->callbackUrl : $callbackUrl,
+            'scope'				      => $scope,
+            'state'				      => $this->getState($state),
+            'display'			     => $this->display,
+            'forcelogin'		   => $this->forcelogin,
+            'language'			    => $this->language,
+        ];
+        if (null === $this->loginAgentUrl)
+        {
+            if ('mobile' === $this->display)
+            {
+                return $this->getMobileUrl('oauth2/authorize', $option);
+            }
+            else
+            {
+                return $this->getUrl('oauth2/authorize', $option);
+            }
+        }
+        else
+        {
+            return $this->loginAgentUrl . '?' . $this->http_build_query($option);
+        }
+    }
+
+    /**
+     * 第二步:处理回调并获取access_token。与getAccessToken不同的是会验证state值是否匹配,防止csrf攻击。
+     *
+     * @param string $storeState 存储的正确的state
+     * @param string $code       第一步里$redirectUri地址中传过来的code,为null则通过get参数获取
+     * @param string $state      回调接收到的state,为null则通过get参数获取
+     *
+     * @return string
+     */
+    protected function __getAccessToken($storeState, $code = null, $state = null)
+    {
+        $this->result = $this->http->post($this->getUrl('oauth2/access_token'), [
+            'client_id'		    => $this->appid,
+            'client_secret'	 => $this->appSecret,
+            'grant_type'	    => 'authorization_code',
+            'code'			        => isset($code) ? $code : (isset($_GET['code']) ? $_GET['code'] : ''),
+            'redirect_uri'	  => $this->getRedirectUri(),
+        ])->json(true);
+        if (isset($this->result['error_code']))
+        {
+            throw new ApiException($this->result['error'], $this->result['error_code']);
+        }
+        else
+        {
+            $this->openid = $this->result['uid'];
+
+            return $this->accessToken = $this->result['access_token'];
+        }
+    }
+
+    /**
+     * 获取用户资料.
+     *
+     * @param string $accessToken
+     *
+     * @return array
+     */
+    public function getUserInfo($accessToken = null)
+    {
+        $this->result = $this->http->get($this->getUrl('2/users/show.json', [
+            'access_token'	 => null === $accessToken ? $this->accessToken : $accessToken,
+            'uid'			        => $this->openid,
+            'screenName'	   => $this->screenName,
+        ]))->json(true);
+        if (isset($this->result['error_code']))
+        {
+            throw new ApiException($this->result['error'], $this->result['error_code']);
+        }
+        else
+        {
+            return $this->result;
+        }
+    }
+
+    /**
+     * 刷新AccessToken续期
+     *
+     * @param string $refreshToken
+     *
+     * @return bool
+     */
+    public function refreshToken($refreshToken)
+    {
+        // 微博不支持刷新
+        return false;
+    }
+
+    /**
+     * 检验授权凭证AccessToken是否有效.
+     *
+     * @param string $accessToken
+     *
+     * @return bool
+     */
+    public function validateAccessToken($accessToken = null)
+    {
+        $this->result = $this->http->post($this->getUrl('oauth2/get_token_info'), [
+            'access_token'	 => null === $accessToken ? $this->accessToken : $accessToken,
+        ])->json(true);
+        if (isset($this->result['error_code']))
+        {
+            throw new ApiException($this->result['error'], $this->result['error_code']);
+        }
+        else
+        {
+            return $this->result['expire_in'] > 0;
+        }
+    }
+}

+ 100 - 0
vendor/yurunsoft/yurun-oauth-login/src/Weibo/loginAgent.html

@@ -0,0 +1,100 @@
+<!DOCTYPE html>
+<html>
+
+    <head>
+        <meta charset="UTF-8">
+        <title>微博登录</title>
+    </head>
+
+    <body>
+        <script>
+            var GWC = {
+                version: '1.0.0',
+                urlParams: {},
+                appendParams: function(url, params) {
+                    if (params) {
+                        var baseWithSearch = url.split('#')[0];
+                        var hash = url.split('#')[1];
+                        for (var key in params) {
+                            var attrValue = params[key];
+                            if (attrValue !== undefined) {
+                                var newParam = key + "=" + attrValue;
+                                if (baseWithSearch.indexOf('?') > 0) {
+                                    var oldParamReg = new RegExp('^' + key + '=[-%.!~*\'\(\)\\w]*', 'g');
+                                    if (oldParamReg.test(baseWithSearch)) {
+                                        baseWithSearch = baseWithSearch.replace(oldParamReg, newParam);
+                                    } else {
+                                        baseWithSearch += "&" + newParam;
+                                    }
+                                } else {
+                                    baseWithSearch += "?" + newParam;
+                                }
+                            }
+                        }
+
+                        if (hash) {
+                            url = baseWithSearch + '#' + hash;
+                        } else {
+                            url = baseWithSearch;
+                        }
+                    }
+                    return url;
+                },
+                getUrlParams: function() {
+                    var pairs = location.search.substring(1).split('&');
+                    for (var i = 0; i < pairs.length; i++) {
+                        var pos = pairs[i].indexOf('=');
+                        if (pos === -1) {
+                            continue;
+                        }
+                        GWC.urlParams[pairs[i].substring(0, pos)] = decodeURIComponent(pairs[i].substring(pos + 1));
+                    }
+                },
+                doRedirect: function() {
+					var code = GWC.urlParams['code'];
+                    var appId = GWC.urlParams['client_id'];
+                    var scope = GWC.urlParams['scope'];
+                    var state = GWC.urlParams['state'];
+                    var display = GWC.urlParams['display'];
+                    var forcelogin = GWC.urlParams['forcelogin'];
+                    var language = GWC.urlParams['language'];
+                    var redirectUri;
+
+                    if (!code) {
+                        //第一步,没有拿到code,跳转至授权页面获取code
+                        var url;
+                        if('mobile' === display)
+                        {
+                            url = 'https://open.weibo.cn/oauth2/authorize';
+                        }
+                        else
+                        {
+                            url = 'https://api.weibo.com/oauth2/authorize';
+                        }
+                        redirectUri = GWC.appendParams(url, {
+                            'client_id': appId,
+                            'redirect_uri': encodeURIComponent(location.href),
+                            'scope': scope,
+                            'state': state,
+                            'display': display,
+                            'forcelogin': forcelogin,
+                            'language': language,
+                        });
+                    } else {
+                        //第二步,从授权页面跳转回来,已经获取到了code,再次跳转到实际所需页面
+                        redirectUri = GWC.appendParams(GWC.urlParams['redirect_uri'], {
+                            'code': code,
+                            'state': state
+                        });
+                    }
+
+                    location.href = redirectUri;
+                }
+            };
+
+            GWC.getUrlParams();
+            GWC.doRedirect();
+        </script>
+    </body>
+
+</html>

+ 285 - 0
vendor/yurunsoft/yurun-oauth-login/src/Weixin/OAuth2.php

@@ -0,0 +1,285 @@
+<?php
+
+namespace Yurun\OAuthLogin\Weixin;
+
+use Yurun\OAuthLogin\ApiException;
+use Yurun\OAuthLogin\Base;
+
+class OAuth2 extends Base
+{
+    /**
+     * api接口域名.
+     */
+    const API_DOMAIN = 'https://api.weixin.qq.com/';
+
+    /**
+     * 开放平台域名.
+     */
+    const OPEN_DOMAIN = 'https://open.weixin.qq.com/';
+
+    /**
+     * 语言,默认为zh_CN.
+     *
+     * @var string
+     */
+    public $lang = 'zh_CN';
+
+    /**
+     * openid从哪个字段取,默认为openid.
+     *
+     * @var int
+     */
+    public $openidMode = OpenidMode::OPEN_ID;
+
+    /**
+     * 获取url地址
+     *
+     * @param string $name   跟在域名后的文本
+     * @param array  $params GET参数
+     *
+     * @return string
+     */
+    public function getUrl($name, $params = [])
+    {
+        if ('http' === substr($name, 0, 4))
+        {
+            $domain = $name;
+        }
+        else
+        {
+            $domain = static::API_DOMAIN . $name;
+        }
+
+        return $domain . (empty($params) ? '' : ('?' . $this->http_build_query($params)));
+    }
+
+    /**
+     * 第一步:获取PC页登录所需的url,一般用于生成二维码
+     *
+     * @param string $callbackUrl 登录回调地址
+     * @param string $state       状态值,不传则自动生成,随后可以通过->state获取。用于第三方应用防止CSRF攻击,成功授权后回调时会原样带回。一般为每个用户登录时随机生成state存在session中,登录回调中判断state是否和session中相同
+     * @param array  $scope       请求用户授权时向用户显示的可进行授权的列表。可空,默认snsapi_login
+     *
+     * @return string
+     */
+    public function getAuthUrl($callbackUrl = null, $state = null, $scope = null)
+    {
+        $option = [
+            'appid'				       => $this->appid,
+            'redirect_uri'		  => null === $callbackUrl ? (null === $this->callbackUrl ? (isset($_SERVER['HTTP_REFERER']) ? $_SERVER['HTTP_REFERER'] : '') : $this->callbackUrl) : $callbackUrl,
+            'response_type'		 => 'code',
+            'scope'				       => null === $scope ? (null === $this->scope ? 'snsapi_login' : $this->scope) : $scope,
+            'state'				       => $this->getState($state),
+        ];
+        if (null === $this->loginAgentUrl)
+        {
+            return $this->getUrl(static::OPEN_DOMAIN . 'connect/qrconnect', $option) . '#wechat_redirect';
+        }
+        else
+        {
+            return $this->loginAgentUrl . '?' . $this->http_build_query($option);
+        }
+    }
+
+    /**
+     * 第一步:获取在微信中登录授权的url.
+     *
+     * @param string $callbackUrl 登录回调地址
+     * @param string $state       状态值,不传则自动生成,随后可以通过->state获取。用于第三方应用防止CSRF攻击,成功授权后回调时会原样带回。一般为每个用户登录时随机生成state存在session中,登录回调中判断state是否和session中相同
+     * @param array  $scope       请求用户授权时向用户显示的可进行授权的列表。可空,默认snsapi_userinfo
+     *
+     * @return string
+     */
+    public function getWeixinAuthUrl($callbackUrl = null, $state = null, $scope = null)
+    {
+        $option = [
+            'appid'				       => $this->appid,
+            'redirect_uri'		  => null === $callbackUrl ? (null === $this->callbackUrl ? (isset($_SERVER['HTTP_REFERER']) ? $_SERVER['HTTP_REFERER'] : '') : $this->callbackUrl) : $callbackUrl,
+            'response_type'		 => 'code',
+            'scope'				       => null === $scope ? (null === $this->scope ? 'snsapi_userinfo' : $this->scope) : $scope,
+            'state'				       => $this->getState($state),
+        ];
+        if (null === $this->loginAgentUrl)
+        {
+            return $this->getUrl(static::OPEN_DOMAIN . 'connect/oauth2/authorize', $option) . '#wechat_redirect';
+        }
+        else
+        {
+            $option['isMp'] = 1;
+
+            return $this->loginAgentUrl . '?' . $this->http_build_query($option);
+        }
+    }
+
+    /**
+     * 第二步:处理回调并获取access_token。与getAccessToken不同的是会验证state值是否匹配,防止csrf攻击。
+     *
+     * @param string $storeState 存储的正确的state
+     * @param string $code       第一步里$redirectUri地址中传过来的code,为null则通过get参数获取
+     * @param string $state      回调接收到的state,为null则通过get参数获取
+     *
+     * @return string
+     */
+    protected function __getAccessToken($storeState, $code = null, $state = null)
+    {
+        $this->result = $this->http->get($this->getUrl('sns/oauth2/access_token', [
+            'appid'			    => $this->appid,
+            'secret'		    => $this->appSecret,
+            'code'			     => isset($code) ? $code : (isset($_GET['code']) ? $_GET['code'] : ''),
+            'grant_type'	 => 'authorization_code',
+        ]))->json(true);
+        if (isset($this->result['errcode']) && 0 != $this->result['errcode'])
+        {
+            throw new ApiException($this->result['errmsg'], $this->result['errcode']);
+        }
+        else
+        {
+            switch ((int) $this->openidMode)
+            {
+                case OpenidMode::OPEN_ID:
+                    $this->openid = $this->result['openid'];
+                    break;
+                case OpenidMode::UNION_ID:
+                    $this->openid = $this->result['unionid'];
+                    break;
+                case OpenidMode::UNION_ID_FIRST:
+                    $this->openid = empty($this->result['unionid']) ? $this->result['openid'] : $this->result['unionid'];
+                    break;
+            }
+
+            return $this->accessToken = $this->result['access_token'];
+        }
+    }
+
+    /**
+     * 获取用户资料.
+     *
+     * @param string $accessToken
+     *
+     * @return array
+     */
+    public function getUserInfo($accessToken = null)
+    {
+        $this->result = $this->http->get($this->getUrl('sns/userinfo', [
+            'access_token'	 => null === $accessToken ? $this->accessToken : $accessToken,
+            'openid'		      => $this->openid,
+            'lang'			       => $this->lang,
+        ]))->json(true);
+        if (isset($this->result['errcode']) && 0 != $this->result['errcode'])
+        {
+            throw new ApiException($this->result['errmsg'], $this->result['errcode']);
+        }
+        else
+        {
+            return $this->result;
+        }
+    }
+
+    /**
+     * 刷新AccessToken续期
+     *
+     * @param string $refreshToken
+     *
+     * @return bool
+     */
+    public function refreshToken($refreshToken)
+    {
+        $this->result = $this->http->get($this->getUrl('sns/oauth2/refresh_token', [
+            'appid'			       => $this->appid,
+            'grant_type'	    => 'refresh_token',
+            'refresh_token'	 => $refreshToken,
+        ]))->json(true);
+
+        return isset($this->result['errcode']) && 0 == $this->result['errcode'];
+    }
+
+    /**
+     * 检验授权凭证AccessToken是否有效.
+     *
+     * @param string $accessToken
+     *
+     * @return bool
+     */
+    public function validateAccessToken($accessToken = null)
+    {
+        $this->result = $this->http->get($this->getUrl('sns/auth', [
+            'access_token'	 => null === $accessToken ? $this->accessToken : $accessToken,
+            'openid'		      => $this->openid,
+        ]))->json(true);
+
+        return isset($this->result['errcode']) && 0 == $this->result['errcode'];
+    }
+
+    /**
+     * 微信小程序登录凭证校验,获取session_key、openid、unionid
+     * 返回session_key
+     * 调用后可以使用$this->result['openid']或$this->result['unionid']获取相应的值
+     *
+     * @param string $jsCode
+     *
+     * @return string
+     */
+    public function getSessionKey($jsCode)
+    {
+        $this->result = $this->http->get($this->getUrl('sns/jscode2session', [
+            'appid'		    => $this->appid,
+            'secret'	    => $this->appSecret,
+            'js_code'	   => $jsCode,
+            'grant_type' => 'authorization_code',
+        ]))->json(true);
+
+        if (isset($this->result['errcode']) && 0 != $this->result['errcode'])
+        {
+            throw new ApiException($this->result['errmsg'], $this->result['errcode']);
+        }
+        else
+        {
+            switch ((int) $this->openidMode)
+            {
+                case OpenidMode::OPEN_ID:
+                    $this->openid = $this->result['openid'];
+                    break;
+                case OpenidMode::UNION_ID:
+                    $this->openid = $this->result['unionid'];
+                    break;
+                case OpenidMode::UNION_ID_FIRST:
+                    $this->openid = empty($this->result['unionid']) ? $this->result['openid'] : $this->result['unionid'];
+                    break;
+            }
+        }
+
+        return $this->result['session_key'];
+    }
+
+    /**
+     * 解密小程序 wx.getUserInfo() 敏感数据.
+     *
+     * @param string $encryptedData
+     * @param string $iv
+     * @param string $sessionKey
+     *
+     * @return array
+     */
+    public function descryptData($encryptedData, $iv, $sessionKey)
+    {
+        if (24 != \strlen($sessionKey))
+        {
+            throw new \InvalidArgumentException('sessionKey 格式错误');
+        }
+        if (24 != \strlen($iv))
+        {
+            throw new \InvalidArgumentException('iv 格式错误');
+        }
+        $aesKey = base64_decode($sessionKey);
+        $aesIV = base64_decode($iv);
+        $aesCipher = base64_decode($encryptedData);
+        $result = openssl_decrypt($aesCipher, 'AES-128-CBC', $aesKey, 1, $aesIV);
+        $dataObj = json_decode($result, true);
+        if (!$dataObj)
+        {
+            throw new \InvalidArgumentException('反序列化数据失败');
+        }
+
+        return $dataObj;
+    }
+}

+ 21 - 0
vendor/yurunsoft/yurun-oauth-login/src/Weixin/OpenidMode.php

@@ -0,0 +1,21 @@
+<?php
+
+namespace Yurun\OAuthLogin\Weixin;
+
+class OpenidMode
+{
+    /**
+     * 使用openid.
+     */
+    const OPEN_ID = 1;
+
+    /**
+     * 使用unionid.
+     */
+    const UNION_ID = 2;
+
+    /**
+     * 优先使用unionid,如果没有则使用openid.
+     */
+    const UNION_ID_FIRST = 3;
+}

+ 88 - 0
vendor/yurunsoft/yurun-oauth-login/src/Weixin/loginAgent.html

@@ -0,0 +1,88 @@
+<!DOCTYPE html>
+<html>
+
+    <head>
+        <meta charset="UTF-8">
+        <title>微信登录</title>
+    </head>
+
+    <body>
+        <script>
+            var GWC = {
+                version: '1.1.1',
+                urlParams: {},
+                appendParams: function(url, params) {
+                    if (params) {
+                        var baseWithSearch = url.split('#')[0];
+                        var hash = url.split('#')[1];
+                        for (var key in params) {
+                            var attrValue = params[key];
+                            if (attrValue !== undefined) {
+                                var newParam = key + "=" + attrValue;
+                                if (baseWithSearch.indexOf('?') > 0) {
+                                    var oldParamReg = new RegExp('^' + key + '=[-%.!~*\'\(\)\\w]*', 'g');
+                                    if (oldParamReg.test(baseWithSearch)) {
+                                        baseWithSearch = baseWithSearch.replace(oldParamReg, newParam);
+                                    } else {
+                                        baseWithSearch += "&" + newParam;
+                                    }
+                                } else {
+                                    baseWithSearch += "?" + newParam;
+                                }
+                            }
+                        }
+                        if (hash) {
+                            url = baseWithSearch + '#' + hash;
+                        } else {
+                            url = baseWithSearch;
+                        }
+                    }
+                    return url;
+                },
+                getUrlParams: function() {
+                    var pairs = location.search.substring(1).split('&');
+                    for (var i = 0; i < pairs.length; i++) {
+                        var pos = pairs[i].indexOf('=');
+                        if (pos === -1) {
+                            continue;
+                        }
+                        GWC.urlParams[pairs[i].substring(0, pos)] = decodeURIComponent(pairs[i].substring(pos + 1));
+                    }
+                },
+                doRedirect: function() {
+                    var code = GWC.urlParams['code'];
+                    var appId = GWC.urlParams['appid'];
+                    var scope = GWC.urlParams['scope'] || 'snsapi_base';
+                    var state = GWC.urlParams['state'];
+                    var isMp = GWC.urlParams['isMp']; //isMp为true时使用开放平台作授权登录,false为网页扫码登录
+                    var baseUrl;
+                    var redirectUri;
+                    if (!code) {
+                        baseUrl = "https://open.weixin.qq.com/connect/oauth2/authorize#wechat_redirect";
+                        if(scope == 'snsapi_login' && !isMp){
+                            baseUrl = "https://open.weixin.qq.com/connect/qrconnect";
+                        }
+                        //第一步,没有拿到code,跳转至微信授权页面获取code
+                        redirectUri = GWC.appendParams(baseUrl, {
+                            'appid': appId,
+                            'redirect_uri': encodeURIComponent(location.href),
+                            'response_type': 'code',
+                            'scope': scope,
+                            'state': state,
+                        });
+                    } else {
+                        //第二步,从微信授权页面跳转回来,已经获取到了code,再次跳转到实际所需页面
+                        redirectUri = GWC.appendParams(GWC.urlParams['redirect_uri'], {
+                            'code': code,
+                            'state': state
+                        });
+                    }
+                    location.href = redirectUri;
+                }
+            };
+            GWC.getUrlParams();
+            GWC.doRedirect();
+        </script>
+    </body>
+
+</html>