auth.py 32 KB

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