You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

705 lines
28KB

  1. # -*- coding: utf-8 -*-
  2. import hmac
  3. import hashlib
  4. import time
  5. from datetime import datetime
  6. from loguru import logger
  7. from . import utils
  8. from .exceptions import ClientError
  9. from .compat import urlquote, to_bytes, is_py2
  10. from .headers import *
  11. from .credentials import StaticCredentialsProvider
  12. AUTH_VERSION_1 = 'v1'
  13. AUTH_VERSION_2 = 'v2'
  14. AUTH_VERSION_4 = 'v4'
  15. DEFAULT_SIGNED_HEADERS = ['content-type', 'content-md5']
  16. def make_auth(access_key_id, access_key_secret, auth_version=AUTH_VERSION_1):
  17. if auth_version == AUTH_VERSION_2:
  18. logger.debug("Init Auth V2: access_key_id: {}, access_key_secret: ******", access_key_id)
  19. return AuthV2(access_key_id.strip(), access_key_secret.strip())
  20. if auth_version == AUTH_VERSION_4:
  21. logger.debug("Init Auth V4: access_key_id: {}, access_key_secret: ******", access_key_id)
  22. return AuthV4(access_key_id.strip(), access_key_secret.strip())
  23. else:
  24. logger.debug("Init Auth v1: access_key_id: {}, access_key_secret: ******", access_key_id)
  25. return Auth(access_key_id.strip(), access_key_secret.strip())
  26. class AuthBase(object):
  27. """用于保存用户AccessKeyId、AccessKeySecret,以及计算签名的对象。"""
  28. __slots__ = "credentials_provider"
  29. def __init__(self, credentials_provider):
  30. self.credentials_provider = credentials_provider
  31. def _sign_rtmp_url(self, url, bucket_name, channel_name, expires, params):
  32. credentials = self.credentials_provider.get_credentials()
  33. if credentials.get_security_token():
  34. params['security-token'] = credentials.get_security_token()
  35. expiration_time = int(time.time()) + expires
  36. canonicalized_resource = "/%s/%s" % (bucket_name, channel_name)
  37. canonicalized_params = []
  38. if params:
  39. items = params.items()
  40. for k, v in items:
  41. if k != "OSSAccessKeyId" and k != "Signature" and k != "Expires" and k != "SecurityToken":
  42. canonicalized_params.append((k, v))
  43. canonicalized_params.sort(key=lambda e: e[0])
  44. canon_params_str = ''
  45. for k, v in canonicalized_params:
  46. canon_params_str += '%s:%s\n' % (k, v)
  47. p = params if params else {}
  48. string_to_sign = str(expiration_time) + "\n" + canon_params_str + canonicalized_resource
  49. logger.debug('Sign Rtmp url: string to be signed = {}', string_to_sign)
  50. h = hmac.new(to_bytes(credentials.get_access_key_secret()), to_bytes(string_to_sign), hashlib.sha1)
  51. signature = utils.b64encode_as_string(h.digest())
  52. p['OSSAccessKeyId'] = credentials.get_access_key_id()
  53. p['Expires'] = str(expiration_time)
  54. p['Signature'] = signature
  55. return url + '?' + '&'.join(_param_to_quoted_query(k, v) for k, v in p.items())
  56. class ProviderAuth(AuthBase):
  57. """签名版本1
  58. 默认构造函数同父类AuthBase,需要传递credentials_provider
  59. """
  60. _subresource_key_set = frozenset(
  61. ['response-content-type', 'response-content-language',
  62. 'response-cache-control', 'logging', 'response-content-encoding',
  63. 'acl', 'uploadId', 'uploads', 'partNumber', 'group', 'link',
  64. 'delete', 'website', 'location', 'objectInfo', 'objectMeta',
  65. 'response-expires', 'response-content-disposition', 'cors', 'lifecycle',
  66. 'restore', 'qos', 'referer', 'stat', 'bucketInfo', 'append', 'position', 'security-token',
  67. 'live', 'comp', 'status', 'vod', 'startTime', 'endTime', 'x-oss-process',
  68. 'symlink', 'callback', 'callback-var', 'tagging', 'encryption', 'versions',
  69. 'versioning', 'versionId', 'policy', 'requestPayment', 'x-oss-traffic-limit', 'qosInfo', 'asyncFetch',
  70. 'x-oss-request-payer', 'sequential', 'inventory', 'inventoryId', 'continuation-token', 'callback',
  71. 'callback-var', 'worm', 'wormId', 'wormExtend', 'replication', 'replicationLocation',
  72. 'replicationProgress', 'transferAcceleration', 'cname', 'metaQuery',
  73. 'x-oss-ac-source-ip', 'x-oss-ac-subnet-mask', 'x-oss-ac-vpc-id', 'x-oss-ac-forward-allow',
  74. 'resourceGroup', 'style', 'styleName', 'x-oss-async-process']
  75. )
  76. def _sign_request(self, req, bucket_name, key):
  77. credentials = self.credentials_provider.get_credentials()
  78. if credentials.get_security_token():
  79. req.headers[OSS_SECURITY_TOKEN] = credentials.get_security_token()
  80. req.headers['date'] = utils.http_date()
  81. signature = self.__make_signature(req, bucket_name, key, credentials)
  82. req.headers['authorization'] = "OSS {0}:{1}".format(credentials.get_access_key_id(), signature)
  83. def _sign_url(self, req, bucket_name, key, expires):
  84. credentials = self.credentials_provider.get_credentials()
  85. if credentials.get_security_token():
  86. req.params['security-token'] = credentials.get_security_token()
  87. expiration_time = int(time.time()) + expires
  88. req.headers['date'] = str(expiration_time)
  89. signature = self.__make_signature(req, bucket_name, key, credentials)
  90. req.params['OSSAccessKeyId'] = credentials.get_access_key_id()
  91. req.params['Expires'] = str(expiration_time)
  92. req.params['Signature'] = signature
  93. return req.url + '?' + '&'.join(_param_to_quoted_query(k, v) for k, v in req.params.items())
  94. def __make_signature(self, req, bucket_name, key, credentials):
  95. if is_py2:
  96. string_to_sign = self.__get_string_to_sign(req, bucket_name, key)
  97. else:
  98. string_to_sign = self.__get_bytes_to_sign(req, bucket_name, key)
  99. logger.debug('Make signature: string to be signed = {}', string_to_sign)
  100. h = hmac.new(to_bytes(credentials.get_access_key_secret()), to_bytes(string_to_sign), hashlib.sha1)
  101. return utils.b64encode_as_string(h.digest())
  102. def __get_string_to_sign(self, req, bucket_name, key):
  103. resource_string = self.__get_resource_string(req, bucket_name, key)
  104. headers_string = self.__get_headers_string(req)
  105. content_md5 = req.headers.get('content-md5', '')
  106. content_type = req.headers.get('content-type', '')
  107. date = req.headers.get('x-oss-date', '') or req.headers.get('date', '')
  108. return '\n'.join([req.method,
  109. content_md5,
  110. content_type,
  111. date,
  112. headers_string + resource_string])
  113. def __get_headers_string(self, req):
  114. headers = req.headers
  115. canon_headers = []
  116. for k, v in headers.items():
  117. lower_key = k.lower()
  118. if lower_key.startswith('x-oss-'):
  119. canon_headers.append((lower_key, v))
  120. canon_headers.sort(key=lambda x: x[0])
  121. if canon_headers:
  122. return '\n'.join(k + ':' + v for k, v in canon_headers) + '\n'
  123. else:
  124. return ''
  125. def __get_resource_string(self, req, bucket_name, key):
  126. if not bucket_name:
  127. return '/' + self.__get_subresource_string(req.params)
  128. else:
  129. return '/{0}/{1}{2}'.format(bucket_name, key, self.__get_subresource_string(req.params))
  130. def __get_subresource_string(self, params):
  131. if not params:
  132. return ''
  133. subresource_params = []
  134. for key, value in params.items():
  135. if key in self._subresource_key_set:
  136. subresource_params.append((key, value))
  137. subresource_params.sort(key=lambda e: e[0])
  138. if subresource_params:
  139. return '?' + '&'.join(self.__param_to_query(k, v) for k, v in subresource_params)
  140. else:
  141. return ''
  142. def __param_to_query(self, k, v):
  143. if v:
  144. return k + '=' + v
  145. else:
  146. return k
  147. def __get_bytes_to_sign(self, req, bucket_name, key):
  148. resource_bytes = self.__get_resource_string(req, bucket_name, key).encode('utf-8')
  149. headers_bytes = self.__get_headers_bytes(req)
  150. content_md5 = req.headers.get('content-md5', '').encode('utf-8')
  151. content_type = req.headers.get('content-type', '').encode('utf-8')
  152. date = req.headers.get('x-oss-date', '').encode('utf-8') or req.headers.get('date', '').encode('utf-8')
  153. return b'\n'.join([req.method.encode('utf-8'),
  154. content_md5,
  155. content_type,
  156. date,
  157. headers_bytes + resource_bytes])
  158. def __get_headers_bytes(self, req):
  159. headers = req.headers
  160. canon_headers = []
  161. for k, v in headers.items():
  162. lower_key = k.lower()
  163. if lower_key.startswith('x-oss-'):
  164. canon_headers.append((lower_key, v))
  165. canon_headers.sort(key=lambda x: x[0])
  166. if canon_headers:
  167. return b'\n'.join(to_bytes(k) + b':' + to_bytes(v) for k, v in canon_headers) + b'\n'
  168. else:
  169. return b''
  170. class Auth(ProviderAuth):
  171. """签名版本1
  172. """
  173. def __init__(self, access_key_id, access_key_secret):
  174. credentials_provider = StaticCredentialsProvider(access_key_id.strip(), access_key_secret.strip())
  175. super(Auth, self).__init__(credentials_provider)
  176. class AnonymousAuth(object):
  177. """用于匿名访问。
  178. .. note::
  179. 匿名用户只能读取public-read的Bucket,或只能读取、写入public-read-write的Bucket。
  180. 不能进行Service、Bucket相关的操作,也不能罗列文件等。
  181. """
  182. def _sign_request(self, req, bucket_name, key):
  183. pass
  184. def _sign_url(self, req, bucket_name, key, expires):
  185. return req.url + '?' + '&'.join(_param_to_quoted_query(k, v) for k, v in req.params.items())
  186. def _sign_rtmp_url(self, url, bucket_name, channel_name, expires, params):
  187. return url + '?' + '&'.join(_param_to_quoted_query(k, v) for k, v in params.items())
  188. class StsAuth(object):
  189. """用于STS临时凭证访问。可以通过官方STS客户端获得临时密钥(AccessKeyId、AccessKeySecret)以及临时安全令牌(SecurityToken)。
  190. 注意到临时凭证会在一段时间后过期,在此之前需要重新获取临时凭证,并更新 :class:`Bucket <oss2.Bucket>` 的 `auth` 成员变量为新
  191. 的 `StsAuth` 实例。
  192. :param str access_key_id: 临时AccessKeyId
  193. :param str access_key_secret: 临时AccessKeySecret
  194. :param str security_token: 临时安全令牌(SecurityToken)
  195. :param str auth_version: 需要生成auth的版本,默认为AUTH_VERSION_1(v1)
  196. """
  197. def __init__(self, access_key_id, access_key_secret, security_token, auth_version=AUTH_VERSION_1):
  198. logger.debug(
  199. "Init StsAuth: access_key_id: {}, access_key_secret: ******, security_token: ******", access_key_id)
  200. credentials_provider = StaticCredentialsProvider(access_key_id, access_key_secret, security_token)
  201. if auth_version == AUTH_VERSION_2:
  202. self.__auth = ProviderAuthV2(credentials_provider)
  203. elif auth_version == AUTH_VERSION_4:
  204. self.__auth = ProviderAuthV4(credentials_provider)
  205. else:
  206. self.__auth = ProviderAuth(credentials_provider)
  207. def _sign_request(self, req, bucket_name, key):
  208. self.__auth._sign_request(req, bucket_name, key)
  209. def _sign_url(self, req, bucket_name, key, expires):
  210. return self.__auth._sign_url(req, bucket_name, key, expires)
  211. def _sign_rtmp_url(self, url, bucket_name, channel_name, expires, params):
  212. return self.__auth._sign_rtmp_url(url, bucket_name, channel_name, expires, params)
  213. def _param_to_quoted_query(k, v):
  214. if v:
  215. return urlquote(k, '') + '=' + urlquote(v, '')
  216. else:
  217. return urlquote(k, '')
  218. def v2_uri_encode(raw_text):
  219. raw_text = to_bytes(raw_text)
  220. res = ''
  221. for b in raw_text:
  222. if isinstance(b, int):
  223. c = chr(b)
  224. else:
  225. c = b
  226. if (c >= 'A' and c <= 'Z') or (c >= 'a' and c <= 'z') \
  227. or (c >= '0' and c <= '9') or c in ['_', '-', '~', '.']:
  228. res += c
  229. else:
  230. res += "%{0:02X}".format(ord(c))
  231. return res
  232. _DEFAULT_ADDITIONAL_HEADERS = set(['range',
  233. 'if-modified-since'])
  234. class ProviderAuthV2(AuthBase):
  235. """签名版本2,默认构造函数同父类AuthBase,需要传递credentials_provider
  236. 与版本1的区别在:
  237. 1. 使用SHA256算法,具有更高的安全性
  238. 2. 参数计算包含所有的HTTP查询参数
  239. """
  240. def _sign_request(self, req, bucket_name, key, in_additional_headers=None):
  241. """把authorization放入req的header里面
  242. :param req: authorization信息将会加入到这个请求的header里面
  243. :type req: oss2.http.Request
  244. :param bucket_name: bucket名称
  245. :param key: OSS文件名
  246. :param in_additional_headers: 加入签名计算的额外header列表
  247. """
  248. credentials = self.credentials_provider.get_credentials()
  249. if credentials.get_security_token():
  250. req.headers[OSS_SECURITY_TOKEN] = credentials.get_security_token()
  251. if in_additional_headers is None:
  252. in_additional_headers = _DEFAULT_ADDITIONAL_HEADERS
  253. additional_headers = self.__get_additional_headers(req, in_additional_headers)
  254. req.headers['date'] = utils.http_date()
  255. signature = self.__make_signature(req, bucket_name, key, additional_headers, credentials)
  256. if additional_headers:
  257. req.headers['authorization'] = "OSS2 AccessKeyId:{0},AdditionalHeaders:{1},Signature:{2}" \
  258. .format(credentials.get_access_key_id(), ';'.join(additional_headers), signature)
  259. else:
  260. req.headers['authorization'] = "OSS2 AccessKeyId:{0},Signature:{1}".format(credentials.get_access_key_id(),
  261. signature)
  262. def _sign_url(self, req, bucket_name, key, expires, in_additional_headers=None):
  263. """返回一个签过名的URL
  264. :param req: 需要签名的请求
  265. :type req: oss2.http.Request
  266. :param bucket_name: bucket名称
  267. :param key: OSS文件名
  268. :param int expires: 返回的url将在`expires`秒后过期.
  269. :param in_additional_headers: 加入签名计算的额外header列表
  270. :return: a signed URL
  271. """
  272. credentials = self.credentials_provider.get_credentials()
  273. if credentials.get_security_token():
  274. req.params['security-token'] = credentials.get_security_token()
  275. if in_additional_headers is None:
  276. in_additional_headers = set()
  277. additional_headers = self.__get_additional_headers(req, in_additional_headers)
  278. expiration_time = int(time.time()) + expires
  279. req.headers['date'] = str(expiration_time) # re-use __make_signature by setting the 'date' header
  280. req.params['x-oss-signature-version'] = 'OSS2'
  281. req.params['x-oss-expires'] = str(expiration_time)
  282. req.params['x-oss-access-key-id'] = credentials.get_access_key_id()
  283. signature = self.__make_signature(req, bucket_name, key, additional_headers, credentials)
  284. req.params['x-oss-signature'] = signature
  285. return req.url + '?' + '&'.join(_param_to_quoted_query(k, v) for k, v in req.params.items())
  286. def __make_signature(self, req, bucket_name, key, additional_headers, credentials):
  287. if is_py2:
  288. string_to_sign = self.__get_string_to_sign(req, bucket_name, key, additional_headers)
  289. else:
  290. string_to_sign = self.__get_bytes_to_sign(req, bucket_name, key, additional_headers)
  291. logger.debug('Make signature: string to be signed = {}', string_to_sign)
  292. h = hmac.new(to_bytes(credentials.get_access_key_secret()), to_bytes(string_to_sign), hashlib.sha256)
  293. return utils.b64encode_as_string(h.digest())
  294. def __get_additional_headers(self, req, in_additional_headers):
  295. # we add a header into additional_headers only if it is already in req's headers.
  296. additional_headers = set(h.lower() for h in in_additional_headers)
  297. keys_in_header = set(k.lower() for k in req.headers.keys())
  298. return additional_headers & keys_in_header
  299. def __get_string_to_sign(self, req, bucket_name, key, additional_header_list):
  300. verb = req.method
  301. content_md5 = req.headers.get('content-md5', '')
  302. content_type = req.headers.get('content-type', '')
  303. date = req.headers.get('date', '')
  304. canonicalized_oss_headers = self.__get_canonicalized_oss_headers(req, additional_header_list)
  305. additional_headers = ';'.join(sorted(additional_header_list))
  306. canonicalized_resource = self.__get_resource_string(req, bucket_name, key)
  307. return verb + '\n' + \
  308. content_md5 + '\n' + \
  309. content_type + '\n' + \
  310. date + '\n' + \
  311. canonicalized_oss_headers + \
  312. additional_headers + '\n' + \
  313. canonicalized_resource
  314. def __get_resource_string(self, req, bucket_name, key):
  315. if bucket_name:
  316. encoded_uri = v2_uri_encode('/' + bucket_name + '/' + key)
  317. else:
  318. encoded_uri = v2_uri_encode('/')
  319. logger.info('encoded_uri={} key={}', encoded_uri, key)
  320. return encoded_uri + self.__get_canonalized_query_string(req)
  321. def __get_canonalized_query_string(self, req):
  322. encoded_params = {}
  323. for param, value in req.params.items():
  324. encoded_params[v2_uri_encode(param)] = v2_uri_encode(value)
  325. if not encoded_params:
  326. return ''
  327. sorted_params = sorted(encoded_params.items(), key=lambda e: e[0])
  328. return '?' + '&'.join(self.__param_to_query(k, v) for k, v in sorted_params)
  329. def __param_to_query(self, k, v):
  330. if v:
  331. return k + '=' + v
  332. else:
  333. return k
  334. def __get_canonicalized_oss_headers(self, req, additional_headers):
  335. """
  336. :param additional_headers: 小写的headers列表, 并且这些headers都不以'x-oss-'为前缀.
  337. """
  338. canon_headers = []
  339. for k, v in req.headers.items():
  340. lower_key = k.lower()
  341. if lower_key.startswith('x-oss-') or lower_key in additional_headers:
  342. canon_headers.append((lower_key, v))
  343. canon_headers.sort(key=lambda x: x[0])
  344. return ''.join(v[0] + ':' + v[1] + '\n' for v in canon_headers)
  345. def __get_bytes_to_sign(self, req, bucket_name, key, additional_header_list):
  346. verb = req.method.encode('utf-8')
  347. content_md5 = req.headers.get('content-md5', '').encode('utf-8')
  348. content_type = req.headers.get('content-type', '').encode('utf-8')
  349. date = req.headers.get('date', '').encode('utf-8')
  350. canonicalized_oss_headers = self.__get_canonicalized_oss_headers_bytes(req, additional_header_list)
  351. additional_headers = ';'.join(sorted(additional_header_list)).encode('utf-8')
  352. canonicalized_resource = self.__get_resource_string(req, bucket_name, key).encode('utf-8')
  353. return verb + b'\n' + \
  354. content_md5 + b'\n' + \
  355. content_type + b'\n' + \
  356. date + b'\n' + \
  357. canonicalized_oss_headers + \
  358. additional_headers + b'\n' + \
  359. canonicalized_resource
  360. def __get_canonicalized_oss_headers_bytes(self, req, additional_headers):
  361. """
  362. :param additional_headers: 小写的headers列表, 并且这些headers都不以'x-oss-'为前缀.
  363. """
  364. canon_headers = []
  365. for k, v in req.headers.items():
  366. lower_key = k.lower()
  367. if lower_key.startswith('x-oss-') or lower_key in additional_headers:
  368. canon_headers.append((lower_key, v))
  369. canon_headers.sort(key=lambda x: x[0])
  370. return b''.join(to_bytes(v[0]) + b':' + to_bytes(v[1]) + b'\n' for v in canon_headers)
  371. class AuthV2(ProviderAuthV2):
  372. """签名版本2,与版本1的区别在:
  373. 1. 使用SHA256算法,具有更高的安全性
  374. 2. 参数计算包含所有的HTTP查询参数
  375. """
  376. def __init__(self, access_key_id, access_key_secret):
  377. credentials_provider = StaticCredentialsProvider(access_key_id.strip(), access_key_secret.strip())
  378. super(AuthV2, self).__init__(credentials_provider)
  379. class ProviderAuthV4(AuthBase):
  380. """签名版本4,默认构造函数同父类AuthBase,需要传递credentials_provider
  381. 与版本2的区别在:
  382. 1. v4 签名规则引入了scope概念,SignToString(待签名串) 和 SigningKey (签名密钥)都需要包含 region信息
  383. 2. 资源路径里的 / 不做转义。 query里的 / 需要转义为 %2F
  384. """
  385. def _sign_request(self, req, bucket_name, key, in_additional_headers=None):
  386. """把authorization放入req的header里面
  387. :param req: authorization信息将会加入到这个请求的header里面
  388. :type req: oss2.http.Request
  389. :param bucket_name: bucket名称
  390. :param key: OSS文件名
  391. :param in_additional_headers: 加入签名计算的额外header列表
  392. """
  393. if req.region is None:
  394. raise ClientError('The region should not be None in signature version 4.')
  395. credentials = self.credentials_provider.get_credentials()
  396. if credentials.get_security_token():
  397. req.headers[OSS_SECURITY_TOKEN] = credentials.get_security_token()
  398. now_datetime = datetime.utcnow()
  399. now_datetime_iso8601 = now_datetime.strftime("%Y%m%dT%H%M%SZ")
  400. now_date = now_datetime_iso8601[:8]
  401. req.headers['x-oss-date'] = now_datetime_iso8601
  402. req.headers['x-oss-content-sha256'] = 'UNSIGNED-PAYLOAD'
  403. additional_signed_headers = self.__get_additional_signed_headers(in_additional_headers)
  404. credential = credentials.get_access_key_id() + "/" + self.__get_scope(now_date, req)
  405. signature = self.__make_signature(req, bucket_name, key, additional_signed_headers, credentials)
  406. authorization = 'OSS4-HMAC-SHA256 Credential={0}, Signature={1}'.format(credential, signature)
  407. if additional_signed_headers:
  408. authorization = authorization + ', AdditionalHeaders={0}'.format(';'.join(additional_signed_headers))
  409. req.headers['authorization'] = authorization
  410. def _sign_url(self, req, bucket_name, key, expires, in_additional_headers=None):
  411. """返回一个签过名的URL
  412. :param req: 需要签名的请求
  413. :type req: oss2.http.Request
  414. :param bucket_name: bucket名称
  415. :param key: OSS文件名
  416. :param int expires: 返回的url将在`expires`秒后过期.
  417. :param in_additional_headers: 加入签名计算的额外header列表
  418. :return: a signed URL
  419. """
  420. raise ClientError("sign_url is not support in signature version 4.")
  421. def __make_signature(self, req, bucket_name, key, additional_signed_headers, credentials):
  422. canonical_request = self.__get_canonical_request(req, bucket_name, key, additional_signed_headers)
  423. string_to_sign = self.__get_string_to_sign(req, canonical_request)
  424. signing_key = self.__get_signing_key(req, credentials)
  425. signature = hmac.new(signing_key, to_bytes(string_to_sign), hashlib.sha256).hexdigest()
  426. # print("canonical_request:\n" + canonical_request)
  427. # print("string_to_sign:\n" + string_to_sign)
  428. logger.debug('Make signature: canonical_request = {}', canonical_request)
  429. logger.debug('Make signature: string to be signed = {}', string_to_sign)
  430. return signature
  431. def __get_additional_signed_headers(self, in_additional_headers):
  432. if in_additional_headers is None:
  433. return None
  434. headers = []
  435. for k in in_additional_headers:
  436. key = k.lower()
  437. if not (key.startswith('x-oss-') or DEFAULT_SIGNED_HEADERS.__contains__(key)):
  438. headers.append(key)
  439. headers.sort(key=lambda x: x[0])
  440. return headers
  441. def __get_canonical_uri(self, bucket_name, key):
  442. if bucket_name:
  443. encoded_uri = '/' + bucket_name + '/' + key
  444. else:
  445. encoded_uri = '/'
  446. return self.__v4_uri_encode(encoded_uri, True)
  447. def __param_to_query(self, k, v):
  448. if v:
  449. return k + '=' + v
  450. else:
  451. return k
  452. def __get_canonical_query(self, req):
  453. encoded_params = {}
  454. for param, value in req.params.items():
  455. encoded_params[self.__v4_uri_encode(param, False)] = self.__v4_uri_encode(value, False)
  456. if not encoded_params:
  457. return ''
  458. sorted_params = sorted(encoded_params.items(), key=lambda e: e[0])
  459. return '&'.join(self.__param_to_query(k, v) for k, v in sorted_params)
  460. def __is_sign_header(self, key, additional_headers):
  461. if key is not None:
  462. if key.startswith('x-oss-'):
  463. return True
  464. if DEFAULT_SIGNED_HEADERS.__contains__(key):
  465. return True
  466. if additional_headers is not None and additional_headers.__contains__(key):
  467. return True
  468. return False
  469. def __get_canonical_headers(self, req, additional_headers):
  470. canon_headers = []
  471. for k, v in req.headers.items():
  472. lower_key = k.lower()
  473. if self.__is_sign_header(lower_key, additional_headers):
  474. canon_headers.append((lower_key, v))
  475. canon_headers.sort(key=lambda x: x[0])
  476. return ''.join(v[0] + ':' + v[1] + '\n' for v in canon_headers)
  477. def __get_canonical_additional_signed_headers(self, additional_headers):
  478. if additional_headers is None:
  479. return ''
  480. return ';'.join(sorted(additional_headers))
  481. def __get_canonical_hash_payload(self, req):
  482. if req.headers.__contains__('x-oss-content-sha256'):
  483. return req.headers.get('x-oss-content-sha256', '')
  484. return 'UNSIGNED-PARYLOAD'
  485. def __get_region(self, req):
  486. return req.cloudbox_id or req.region
  487. def __get_product(self, req):
  488. return req.product
  489. def __get_scope(self, date, req):
  490. return date + "/" + self.__get_region(req) + "/" + self.__get_product(req) + "/aliyun_v4_request"
  491. def __get_canonical_request(self, req, bucket_name, key, additional_signed_headers):
  492. return req.method + '\n' + \
  493. self.__get_canonical_uri(bucket_name, key) + '\n' + \
  494. self.__get_canonical_query(req) + '\n' + \
  495. self.__get_canonical_headers(req, additional_signed_headers) + '\n' + \
  496. self.__get_canonical_additional_signed_headers(additional_signed_headers) + '\n' + \
  497. self.__get_canonical_hash_payload(req)
  498. def __get_string_to_sign(self, req, canonical_request):
  499. datetime = req.headers.get('x-oss-date', '')
  500. date = datetime[:8]
  501. return 'OSS4-HMAC-SHA256' + '\n' + \
  502. datetime + '\n' + \
  503. self.__get_scope(date, req) + '\n' + \
  504. hashlib.sha256(to_bytes(canonical_request)).hexdigest()
  505. def __get_signing_key(self, req, credentials):
  506. date = req.headers.get('x-oss-date', '')[:8]
  507. key_secret = 'aliyun_v4' + credentials.get_access_key_secret()
  508. signing_date = hmac.new(to_bytes(key_secret), to_bytes(date), hashlib.sha256)
  509. signing_region = hmac.new(signing_date.digest(), to_bytes(self.__get_region(req)), hashlib.sha256)
  510. signing_product = hmac.new(signing_region.digest(), to_bytes(self.__get_product(req)), hashlib.sha256)
  511. signing_key = hmac.new(signing_product.digest(), to_bytes('aliyun_v4_request'), hashlib.sha256)
  512. return signing_key.digest()
  513. def __v4_uri_encode(self, raw_text, ignoreSlashes):
  514. raw_text = to_bytes(raw_text)
  515. res = ''
  516. for b in raw_text:
  517. if isinstance(b, int):
  518. c = chr(b)
  519. else:
  520. c = b
  521. if (c >= 'A' and c <= 'Z') or (c >= 'a' and c <= 'z') \
  522. or (c >= '0' and c <= '9') or c in ['_', '-', '~', '.']:
  523. res += c
  524. elif ignoreSlashes is True and c == '/':
  525. res += c
  526. else:
  527. res += "%{0:02X}".format(ord(c))
  528. return res
  529. class AuthV4(ProviderAuthV4):
  530. """签名版本4,与版本2的区别在:
  531. 1. v4 签名规则引入了scope概念,SignToString(待签名串) 和 SigningKey (签名密钥)都需要包含 region信息
  532. 2. 资源路径里的 / 不做转义。 query里的 / 需要转义为 %2F
  533. """
  534. def __init__(self, access_key_id, access_key_secret):
  535. credentials_provider = StaticCredentialsProvider(access_key_id.strip(), access_key_secret.strip())
  536. super(AuthV4, self).__init__(credentials_provider)