Client.php 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447
  1. <?php
  2. /**
  3. * +----------------------------------------------------------------------
  4. * | CRMEB [ CRMEB赋能开发者,助力企业发展 ]
  5. * +----------------------------------------------------------------------
  6. * | Copyright (c) 2016~2022 https://www.crmeb.com All rights reserved.
  7. * +----------------------------------------------------------------------
  8. * | Licensed CRMEB并不是自由软件,未经许可不能去掉CRMEB相关版权
  9. * +----------------------------------------------------------------------
  10. * | Author: CRMEB Team <admin@crmeb.com>
  11. * +----------------------------------------------------------------------
  12. */
  13. namespace crmeb\services\upload\extend\jdoss;
  14. use AsyncAws\Core\Request;
  15. use crmeb\exceptions\UploadException;
  16. use crmeb\services\upload\BaseClient;
  17. use GuzzleHttp\Psr7\Utils;
  18. /**
  19. * 京东云上传
  20. * Class Client
  21. * @package crmeb\services\upload\extend\jdoss
  22. */
  23. class Client extends BaseClient
  24. {
  25. const ALGORITHM_REQUEST = 'AWS4-HMAC-SHA256';
  26. const BLACKLIST_HEADERS = [
  27. 'cache-control' => true,
  28. 'content-type' => true,
  29. 'content-length' => true,
  30. 'expect' => true,
  31. 'max-forwards' => true,
  32. 'pragma' => true,
  33. 'range' => true,
  34. 'te' => true,
  35. 'if-match' => true,
  36. 'if-none-match' => true,
  37. 'if-modified-since' => true,
  38. 'if-unmodified-since' => true,
  39. 'if-range' => true,
  40. 'accept' => true,
  41. 'authorization' => true,
  42. 'proxy-authorization' => true,
  43. 'from' => true,
  44. 'referer' => true,
  45. 'user-agent' => true,
  46. 'x-amzn-trace-id' => true,
  47. 'aws-sdk-invocation-id' => true,
  48. 'aws-sdk-retry' => true,
  49. ];
  50. /**
  51. * AK
  52. * @var
  53. */
  54. protected $accessKeyId;
  55. /**
  56. * SK
  57. * @var
  58. */
  59. protected $secretKey;
  60. /**
  61. * 桶名
  62. * @var string
  63. */
  64. protected $bucketName;
  65. /**
  66. * 地区
  67. * @var string
  68. */
  69. protected $region;
  70. /**
  71. * @var mixed|string
  72. */
  73. protected $uploadUrl;
  74. /**
  75. * @var string
  76. */
  77. protected $baseUrl = 's3.<REGION>.jdcloud-oss.com';
  78. //默认地域
  79. const DEFAULT_REGION = 'cn-north-1';
  80. /**
  81. * Client constructor.
  82. * @param array $config
  83. */
  84. public function __construct(array $config = [])
  85. {
  86. $this->accessKeyId = $config['accessKey'] ?? '';
  87. $this->secretKey = $config['secretKey'] ?? '';
  88. $this->bucketName = $config['bucket'] ?? '';
  89. $this->region = $config['region'] ?? self::DEFAULT_REGION;
  90. $this->uploadUrl = $config['uploadUrl'] ?? '';
  91. }
  92. /**
  93. * 上传
  94. * @param string $bucket
  95. * @param string $region
  96. * @param string $key
  97. * @param array $data
  98. * @return array|\crmeb\services\upload\extend\cos\SimpleXMLElement
  99. */
  100. public function putObject(string $bucket, string $region, string $key, array $data)
  101. {
  102. $url = $this->getRequestUrl($bucket, $region);
  103. $header = [
  104. 'Host' => $url,
  105. ];
  106. if (isset($data['body'])) {
  107. $header['Content-Length'] = strlen($data['body']);
  108. }
  109. return $this->request('https://' . $url . '/'. $key, 'PUT', $data, $header);
  110. }
  111. /**
  112. * 删除文件
  113. * @param string $bucket
  114. * @param string $region
  115. * @param string $key
  116. * @return array|\crmeb\services\upload\extend\cos\SimpleXMLElement
  117. */
  118. public function deleteObject(string $bucket, string $region, string $key)
  119. {
  120. $url = $this->getRequestUrl($bucket, $region);
  121. $header = [
  122. 'Host' => $url,
  123. ];
  124. return $this->request('https://' . $url . '/'. $key, 'DELETE', [], $header);
  125. }
  126. /**
  127. * 获取桶列表
  128. * @return array|bool|\crmeb\services\upload\SimpleXMLElement|mixed
  129. */
  130. public function listBuckets()
  131. {
  132. $url = $this->getRequestUrl();
  133. $header = [
  134. 'Host' => $url,
  135. ];
  136. $res = $this->request('https://' . $url . '/', 'GET', [], $header);
  137. return $res;
  138. }
  139. /**
  140. * 检测桶,不存在返回true
  141. * @param string $bucket
  142. * @param string $region
  143. * @return array|bool|\crmeb\services\upload\SimpleXMLElement|mixed
  144. */
  145. public function headBucket(string $bucket, string $region = '')
  146. {
  147. $url = $this->getRequestUrl($bucket, $region);
  148. $header = [
  149. 'Host' => $url
  150. ];
  151. return $this->request('https://' . $url, 'head', [], $header);
  152. }
  153. /**
  154. * 创建桶
  155. * @param $name
  156. * @param $region
  157. * @param $acl
  158. * @return array|\crmeb\services\upload\extend\cos\SimpleXMLElement
  159. */
  160. public function createBucket($name, $region, $acl)
  161. {
  162. $url = $this->getRequestUrl($name, $region);
  163. $header = [
  164. 'Host' => $url,
  165. 'x-amz-acl' => $acl
  166. ];
  167. $res = $this->request('https://' . $url . '/', 'PUT', [], $header);
  168. return $res;
  169. }
  170. /**
  171. * 删除桶
  172. * @param string $bucket
  173. * @param string $region
  174. * @return array|\crmeb\services\upload\extend\cos\SimpleXMLElement
  175. */
  176. public function deleteBucket(string $bucket, string $region = '')
  177. {
  178. $url = $this->getRequestUrl($bucket, $region);
  179. $header = [
  180. 'Host' => $url
  181. ];
  182. return $this->request('https://' . $url . '/', 'DELETE', [], $header);
  183. }
  184. /**
  185. * 设置桶跨域规则
  186. * @param string $bucket
  187. * @param string $region
  188. * @param $data
  189. * @return array|\crmeb\services\upload\extend\cos\SimpleXMLElement
  190. */
  191. public function putBucketCors(string $bucket, string $region = '', $data = [])
  192. {
  193. $url = $this->getRequestUrl($bucket, $region);
  194. $header = [
  195. 'Host' => $url,
  196. 'content-type' => 'application/xml'
  197. ];
  198. return $this->request('https://' . $url . '/?cors', 'PUT', $data, $header);
  199. }
  200. /**
  201. * 获取host
  202. * @param string $bucket
  203. * @param string $region
  204. * @return string
  205. */
  206. public function getRequestUrl(string $bucket = '', string $region = self::DEFAULT_REGION)
  207. {
  208. if (!$this->accessKeyId) {
  209. throw new UploadException('请传入SecretId');
  210. }
  211. if (!$this->secretKey) {
  212. throw new UploadException('请传入SecretKey');
  213. }
  214. return ($bucket ? $bucket . '.' : '') . 's3.' . $region . '.jdcloud-oss.com';
  215. }
  216. /**
  217. * 发起请求
  218. * @param string $url
  219. * @param string $method
  220. * @param array $data
  221. * @param array $clientHeader
  222. * @param int $timeout
  223. * @return array|bool|\crmeb\services\upload\SimpleXMLElement|mixed
  224. */
  225. protected function request(string $url, string $method, array $data = [], array $clientHeader = [], int $timeout = 10)
  226. {
  227. if (!isset($clientHeader['Content-Length'])) {
  228. $clientHeader['Content-Length'] = 0;
  229. }
  230. $clientHeader['x-amz-date'] = gmdate('Ymd\THis\Z');
  231. $clientHeader['x-amz-content-sha256'] = hash('sha256', $data['body'] ?? '', false);
  232. $authorization = $this->generateAwsSignatureV4($data['region'] ?? self::DEFAULT_REGION, $url, $method, $clientHeader, $data);
  233. $clientHeader['Authorization'] = $authorization;
  234. return $this->requestClient($url, $method, $data, $clientHeader, $timeout);
  235. }
  236. /**
  237. * 生成签名
  238. * @param string $region
  239. * @param string $url
  240. * @param string $httpMethod
  241. * @param array $header
  242. * @param array $data
  243. * @param string $service
  244. * @return string
  245. */
  246. protected function generateAwsSignatureV4(string $region, string $url, string $httpMethod, array $header, array $data = [], string $service = 's3')
  247. {
  248. $algorithm = self::ALGORITHM_REQUEST;
  249. $t = new \DateTime('UTC');
  250. $amzDate = $t->format('Ymd\THis\Z');
  251. $dateStamp = $t->format('Ymd');
  252. [$canonicalRequest, $signedHeaders] = $this->createCanonicalRequest($url, $httpMethod, $header, $data);
  253. $credentialScope = $dateStamp . '/' . $region . '/' . $service . '/aws4_request';
  254. $stringToSign = $algorithm . "\n" . $amzDate . "\n" . $credentialScope . "\n" . hash('sha256', $canonicalRequest);
  255. $signingKey = hash_hmac('sha256', 'aws4_request',
  256. hash_hmac('sha256', $service,
  257. hash_hmac('sha256', $region,
  258. hash_hmac('sha256', $dateStamp, 'AWS4' . $this->secretKey, true),
  259. true),
  260. true),
  261. true);
  262. $signature = hash_hmac('sha256', $stringToSign, $signingKey);
  263. return $algorithm . ' Credential=' . $this->accessKeyId . '/' . $credentialScope . ', SignedHeaders=' . $signedHeaders . ', Signature=' . $signature;
  264. }
  265. /**
  266. * @param string $url
  267. * @param string $httpMethod
  268. * @param array $header
  269. * @param array $data
  270. * @return array
  271. */
  272. public function createCanonicalRequest(string $url, string $httpMethod, array $header, array $data = [])
  273. {
  274. $canonicalQueryString = '';
  275. $payload = '';
  276. if (!empty($data['query'])) {
  277. $query = $payload = $data['query'];
  278. ksort($query);
  279. $queryAttr = [];
  280. foreach ($query as $key => $item) {
  281. $queryAttr[urlencode($key)] = urlencode($item);
  282. }
  283. if ($queryAttr) {
  284. $canonicalQueryString = implode('&', $queryAttr);
  285. }
  286. $payload = json_encode($payload);
  287. } elseif (!empty($data['body'])) {
  288. $payload = $data['body'];
  289. } elseif (!empty($data['json'])) {
  290. $payload = json_encode($data['json']);
  291. }
  292. $canonicalHeaders = '';
  293. $signedHeaders = '';
  294. if ($header) {
  295. $canonicalHeadersAtrr = $signedHeadersAttr = [];
  296. ksort($header);
  297. foreach ($header as $key => $item) {
  298. $key = strtolower($key);
  299. if (isset(self::BLACKLIST_HEADERS[$key])) {
  300. continue;
  301. }
  302. $canonicalHeadersAtrr[] = $key . ':' . preg_replace('/\s+/', ' ', trim($item));
  303. $signedHeadersAttr[] = strtolower($key);
  304. }
  305. ksort($canonicalHeadersAtrr);
  306. ksort($signedHeadersAttr);
  307. if ($canonicalHeadersAtrr) {
  308. $canonicalHeaders = implode("\n", $canonicalHeadersAtrr);
  309. $signedHeaders = implode(';', $signedHeadersAttr);
  310. }
  311. }
  312. $canonicalUri = $this->createCanonicalizedPath($url);
  313. $bodyDigest = $this->buildBodyDigest($header, (string)Utils::streamFor($payload));
  314. $canonicalRequest = $httpMethod . "\n" . $canonicalUri . "\n" . $canonicalQueryString . "\n" . $canonicalHeaders . "\n" . "\n" . $signedHeaders . "\n" . $bodyDigest;
  315. return [$canonicalRequest, $signedHeaders];
  316. }
  317. /**
  318. *
  319. * @param string $url
  320. * @return string
  321. */
  322. public function createCanonicalizedPath(string $url)
  323. {
  324. $canonicalUri = '/';
  325. $urlAttr = pathinfo($url);
  326. if (isset($urlAttr['dirname']) && $urlAttr['dirname'] !== 'https:') {
  327. $urlParse = parse_url($urlAttr['dirname'] ?? '');
  328. if (isset($urlParse['path'])) {
  329. $canonicalUri .= substr($urlParse['path'], 1) . '/';
  330. }
  331. if (isset($urlAttr['basename'])) {
  332. $canonicalUri .= $urlAttr['basename'];
  333. }
  334. //|| strlen($canonicalUri) - 1 !== $pos
  335. if (!($pos = strripos($canonicalUri, '/')) ) {
  336. $canonicalUri .= '/';
  337. }
  338. }
  339. return $canonicalUri;
  340. }
  341. /**
  342. * 处理dody hash
  343. * @param array $header
  344. * @param string $body
  345. * @return string
  346. */
  347. protected function buildBodyDigest(array $header, string $body = ''): string
  348. {
  349. if (isset($header['x-amz-content-sha256'])) {
  350. $hash = $header['x-amz-content-sha256'];
  351. } else {
  352. $hash = hash('sha256', $body);
  353. }
  354. return $hash;
  355. }
  356. /**
  357. * 处理bogy
  358. * @param string $body
  359. * @return string
  360. */
  361. public function dechunk(string $body): string
  362. {
  363. $h = fopen('php://temp', 'w+');
  364. stream_filter_append($h, 'dechunk', \STREAM_FILTER_WRITE);
  365. fwrite($h, $body);
  366. $body = stream_get_contents($h, -1, 0);
  367. rewind($h);
  368. ftruncate($h, 0);
  369. return $body;
  370. }
  371. /**
  372. * 获取区域
  373. * @return \string[][]
  374. */
  375. public function getRegion()
  376. {
  377. return [
  378. [
  379. 'value' => 'cn-north-1',
  380. 'label' => '华北-北京',
  381. ],
  382. [
  383. 'value' => 'cn-south-1',
  384. 'label' => '华南-广州',
  385. ],
  386. [
  387. 'value' => 'cn-east-2',
  388. 'label' => '华东-上海',
  389. ],
  390. [
  391. 'value' => 'cn-east-1',
  392. 'label' => '华东-宿迁',
  393. ]
  394. ];
  395. }
  396. }