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.

1099 lines
41 KiB

4 years ago
  1. # Copyright 2010 Google Inc.
  2. # Copyright (c) 2011 Mitch Garnaat http://garnaat.org/
  3. # Copyright (c) 2011, Eucalyptus Systems, Inc.
  4. #
  5. # Permission is hereby granted, free of charge, to any person obtaining a
  6. # copy of this software and associated documentation files (the
  7. # "Software"), to deal in the Software without restriction, including
  8. # without limitation the rights to use, copy, modify, merge, publish, dis-
  9. # tribute, sublicense, and/or sell copies of the Software, and to permit
  10. # persons to whom the Software is furnished to do so, subject to the fol-
  11. # lowing conditions:
  12. #
  13. # The above copyright notice and this permission notice shall be included
  14. # in all copies or substantial portions of the Software.
  15. #
  16. # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
  17. # OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABIL-
  18. # ITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT
  19. # SHALL THE AUTHOR BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
  20. # WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  21. # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
  22. # IN THE SOFTWARE.
  23. """
  24. Handles authentication required to AWS and GS
  25. """
  26. import base64
  27. import boto
  28. import boto.auth_handler
  29. import boto.exception
  30. import boto.plugin
  31. import boto.utils
  32. import copy
  33. import datetime
  34. from email.utils import formatdate
  35. import hmac
  36. import os
  37. import posixpath
  38. from boto.compat import urllib, encodebytes, parse_qs_safe, urlparse
  39. from boto.auth_handler import AuthHandler
  40. from boto.exception import BotoClientError
  41. try:
  42. from hashlib import sha1 as sha
  43. from hashlib import sha256 as sha256
  44. except ImportError:
  45. import sha
  46. sha256 = None
  47. # Region detection strings to determine if SigV2 should be used
  48. # by default
  49. S3_AUTH_DETECT = [
  50. '-ap-northeast-1',
  51. '.ap-northeast-1',
  52. '-ap-southeast-1',
  53. '.ap-southeast-1',
  54. '-ap-southeast-2',
  55. '.ap-southeast-2',
  56. '-eu-west-1',
  57. '.eu-west-1',
  58. '-external-1',
  59. '.external-1',
  60. '-sa-east-1',
  61. '.sa-east-1',
  62. '-us-east-1',
  63. '.us-east-1',
  64. '-us-gov-west-1',
  65. '.us-gov-west-1',
  66. '-us-west-1',
  67. '.us-west-1',
  68. '-us-west-2',
  69. '.us-west-2'
  70. ]
  71. SIGV4_DETECT = [
  72. '.cn-',
  73. # In eu-central and ap-northeast-2 we support both host styles for S3
  74. '.eu-central',
  75. '-eu-central',
  76. '.ap-northeast-2',
  77. '-ap-northeast-2',
  78. '.ap-south-1',
  79. '-ap-south-1',
  80. '.us-east-2',
  81. '-us-east-2',
  82. '-ca-central',
  83. '.ca-central',
  84. '.eu-west-2',
  85. '-eu-west-2',
  86. ]
  87. class HmacKeys(object):
  88. """Key based Auth handler helper."""
  89. def __init__(self, host, config, provider):
  90. if provider.access_key is None or provider.secret_key is None:
  91. raise boto.auth_handler.NotReadyToAuthenticate()
  92. self.host = host
  93. self.update_provider(provider)
  94. def update_provider(self, provider):
  95. self._provider = provider
  96. self._hmac = hmac.new(self._provider.secret_key.encode('utf-8'),
  97. digestmod=sha)
  98. if sha256:
  99. self._hmac_256 = hmac.new(self._provider.secret_key.encode('utf-8'),
  100. digestmod=sha256)
  101. else:
  102. self._hmac_256 = None
  103. def algorithm(self):
  104. if self._hmac_256:
  105. return 'HmacSHA256'
  106. else:
  107. return 'HmacSHA1'
  108. def _get_hmac(self):
  109. if self._hmac_256:
  110. digestmod = sha256
  111. else:
  112. digestmod = sha
  113. return hmac.new(self._provider.secret_key.encode('utf-8'),
  114. digestmod=digestmod)
  115. def sign_string(self, string_to_sign):
  116. new_hmac = self._get_hmac()
  117. new_hmac.update(string_to_sign.encode('utf-8'))
  118. return encodebytes(new_hmac.digest()).decode('utf-8').strip()
  119. def __getstate__(self):
  120. pickled_dict = copy.copy(self.__dict__)
  121. del pickled_dict['_hmac']
  122. del pickled_dict['_hmac_256']
  123. return pickled_dict
  124. def __setstate__(self, dct):
  125. self.__dict__ = dct
  126. self.update_provider(self._provider)
  127. class AnonAuthHandler(AuthHandler, HmacKeys):
  128. """
  129. Implements Anonymous requests.
  130. """
  131. capability = ['anon']
  132. def __init__(self, host, config, provider):
  133. super(AnonAuthHandler, self).__init__(host, config, provider)
  134. def add_auth(self, http_request, **kwargs):
  135. pass
  136. class HmacAuthV1Handler(AuthHandler, HmacKeys):
  137. """ Implements the HMAC request signing used by S3 and GS."""
  138. capability = ['hmac-v1', 's3']
  139. def __init__(self, host, config, provider):
  140. AuthHandler.__init__(self, host, config, provider)
  141. HmacKeys.__init__(self, host, config, provider)
  142. self._hmac_256 = None
  143. def update_provider(self, provider):
  144. super(HmacAuthV1Handler, self).update_provider(provider)
  145. self._hmac_256 = None
  146. def add_auth(self, http_request, **kwargs):
  147. headers = http_request.headers
  148. method = http_request.method
  149. auth_path = http_request.auth_path
  150. if 'Date' not in headers:
  151. headers['Date'] = formatdate(usegmt=True)
  152. if self._provider.security_token:
  153. key = self._provider.security_token_header
  154. headers[key] = self._provider.security_token
  155. string_to_sign = boto.utils.canonical_string(method, auth_path,
  156. headers, None,
  157. self._provider)
  158. boto.log.debug('StringToSign:\n%s' % string_to_sign)
  159. b64_hmac = self.sign_string(string_to_sign)
  160. auth_hdr = self._provider.auth_header
  161. auth = ("%s %s:%s" % (auth_hdr, self._provider.access_key, b64_hmac))
  162. boto.log.debug('Signature:\n%s' % auth)
  163. headers['Authorization'] = auth
  164. class HmacAuthV2Handler(AuthHandler, HmacKeys):
  165. """
  166. Implements the simplified HMAC authorization used by CloudFront.
  167. """
  168. capability = ['hmac-v2', 'cloudfront']
  169. def __init__(self, host, config, provider):
  170. AuthHandler.__init__(self, host, config, provider)
  171. HmacKeys.__init__(self, host, config, provider)
  172. self._hmac_256 = None
  173. def update_provider(self, provider):
  174. super(HmacAuthV2Handler, self).update_provider(provider)
  175. self._hmac_256 = None
  176. def add_auth(self, http_request, **kwargs):
  177. headers = http_request.headers
  178. if 'Date' not in headers:
  179. headers['Date'] = formatdate(usegmt=True)
  180. if self._provider.security_token:
  181. key = self._provider.security_token_header
  182. headers[key] = self._provider.security_token
  183. b64_hmac = self.sign_string(headers['Date'])
  184. auth_hdr = self._provider.auth_header
  185. headers['Authorization'] = ("%s %s:%s" %
  186. (auth_hdr,
  187. self._provider.access_key, b64_hmac))
  188. class HmacAuthV3Handler(AuthHandler, HmacKeys):
  189. """Implements the new Version 3 HMAC authorization used by Route53."""
  190. capability = ['hmac-v3', 'route53', 'ses']
  191. def __init__(self, host, config, provider):
  192. AuthHandler.__init__(self, host, config, provider)
  193. HmacKeys.__init__(self, host, config, provider)
  194. def add_auth(self, http_request, **kwargs):
  195. headers = http_request.headers
  196. if 'Date' not in headers:
  197. headers['Date'] = formatdate(usegmt=True)
  198. if self._provider.security_token:
  199. key = self._provider.security_token_header
  200. headers[key] = self._provider.security_token
  201. b64_hmac = self.sign_string(headers['Date'])
  202. s = "AWS3-HTTPS AWSAccessKeyId=%s," % self._provider.access_key
  203. s += "Algorithm=%s,Signature=%s" % (self.algorithm(), b64_hmac)
  204. headers['X-Amzn-Authorization'] = s
  205. class HmacAuthV3HTTPHandler(AuthHandler, HmacKeys):
  206. """
  207. Implements the new Version 3 HMAC authorization used by DynamoDB.
  208. """
  209. capability = ['hmac-v3-http']
  210. def __init__(self, host, config, provider):
  211. AuthHandler.__init__(self, host, config, provider)
  212. HmacKeys.__init__(self, host, config, provider)
  213. def headers_to_sign(self, http_request):
  214. """
  215. Select the headers from the request that need to be included
  216. in the StringToSign.
  217. """
  218. headers_to_sign = {'Host': self.host}
  219. for name, value in http_request.headers.items():
  220. lname = name.lower()
  221. if lname.startswith('x-amz'):
  222. headers_to_sign[name] = value
  223. return headers_to_sign
  224. def canonical_headers(self, headers_to_sign):
  225. """
  226. Return the headers that need to be included in the StringToSign
  227. in their canonical form by converting all header keys to lower
  228. case, sorting them in alphabetical order and then joining
  229. them into a string, separated by newlines.
  230. """
  231. l = sorted(['%s:%s' % (n.lower().strip(),
  232. headers_to_sign[n].strip()) for n in headers_to_sign])
  233. return '\n'.join(l)
  234. def string_to_sign(self, http_request):
  235. """
  236. Return the canonical StringToSign as well as a dict
  237. containing the original version of all headers that
  238. were included in the StringToSign.
  239. """
  240. headers_to_sign = self.headers_to_sign(http_request)
  241. canonical_headers = self.canonical_headers(headers_to_sign)
  242. string_to_sign = '\n'.join([http_request.method,
  243. http_request.auth_path,
  244. '',
  245. canonical_headers,
  246. '',
  247. http_request.body])
  248. return string_to_sign, headers_to_sign
  249. def add_auth(self, req, **kwargs):
  250. """
  251. Add AWS3 authentication to a request.
  252. :type req: :class`boto.connection.HTTPRequest`
  253. :param req: The HTTPRequest object.
  254. """
  255. # This could be a retry. Make sure the previous
  256. # authorization header is removed first.
  257. if 'X-Amzn-Authorization' in req.headers:
  258. del req.headers['X-Amzn-Authorization']
  259. req.headers['X-Amz-Date'] = formatdate(usegmt=True)
  260. if self._provider.security_token:
  261. req.headers['X-Amz-Security-Token'] = self._provider.security_token
  262. string_to_sign, headers_to_sign = self.string_to_sign(req)
  263. boto.log.debug('StringToSign:\n%s' % string_to_sign)
  264. hash_value = sha256(string_to_sign.encode('utf-8')).digest()
  265. b64_hmac = self.sign_string(hash_value)
  266. s = "AWS3 AWSAccessKeyId=%s," % self._provider.access_key
  267. s += "Algorithm=%s," % self.algorithm()
  268. s += "SignedHeaders=%s," % ';'.join(headers_to_sign)
  269. s += "Signature=%s" % b64_hmac
  270. req.headers['X-Amzn-Authorization'] = s
  271. class HmacAuthV4Handler(AuthHandler, HmacKeys):
  272. """
  273. Implements the new Version 4 HMAC authorization.
  274. """
  275. capability = ['hmac-v4']
  276. def __init__(self, host, config, provider,
  277. service_name=None, region_name=None):
  278. AuthHandler.__init__(self, host, config, provider)
  279. HmacKeys.__init__(self, host, config, provider)
  280. # You can set the service_name and region_name to override the
  281. # values which would otherwise come from the endpoint, e.g.
  282. # <service>.<region>.amazonaws.com.
  283. self.service_name = service_name
  284. self.region_name = region_name
  285. def _sign(self, key, msg, hex=False):
  286. if not isinstance(key, bytes):
  287. key = key.encode('utf-8')
  288. if hex:
  289. sig = hmac.new(key, msg.encode('utf-8'), sha256).hexdigest()
  290. else:
  291. sig = hmac.new(key, msg.encode('utf-8'), sha256).digest()
  292. return sig
  293. def headers_to_sign(self, http_request):
  294. """
  295. Select the headers from the request that need to be included
  296. in the StringToSign.
  297. """
  298. host_header_value = self.host_header(self.host, http_request)
  299. if http_request.headers.get('Host'):
  300. host_header_value = http_request.headers['Host']
  301. headers_to_sign = {'Host': host_header_value}
  302. for name, value in http_request.headers.items():
  303. lname = name.lower()
  304. if lname.startswith('x-amz'):
  305. if isinstance(value, bytes):
  306. value = value.decode('utf-8')
  307. headers_to_sign[name] = value
  308. return headers_to_sign
  309. def host_header(self, host, http_request):
  310. port = http_request.port
  311. secure = http_request.protocol == 'https'
  312. if ((port == 80 and not secure) or (port == 443 and secure)):
  313. return host
  314. return '%s:%s' % (host, port)
  315. def query_string(self, http_request):
  316. parameter_names = sorted(http_request.params.keys())
  317. pairs = []
  318. for pname in parameter_names:
  319. pval = boto.utils.get_utf8_value(http_request.params[pname])
  320. pairs.append(urllib.parse.quote(pname, safe='') + '=' +
  321. urllib.parse.quote(pval, safe='-_~'))
  322. return '&'.join(pairs)
  323. def canonical_query_string(self, http_request):
  324. # POST requests pass parameters in through the
  325. # http_request.body field.
  326. if http_request.method == 'POST':
  327. return ""
  328. l = []
  329. for param in sorted(http_request.params):
  330. value = boto.utils.get_utf8_value(http_request.params[param])
  331. l.append('%s=%s' % (urllib.parse.quote(param, safe='-_.~'),
  332. urllib.parse.quote(value, safe='-_.~')))
  333. return '&'.join(l)
  334. def canonical_headers(self, headers_to_sign):
  335. """
  336. Return the headers that need to be included in the StringToSign
  337. in their canonical form by converting all header keys to lower
  338. case, sorting them in alphabetical order and then joining
  339. them into a string, separated by newlines.
  340. """
  341. canonical = []
  342. for header in headers_to_sign:
  343. c_name = header.lower().strip()
  344. raw_value = str(headers_to_sign[header])
  345. if '"' in raw_value:
  346. c_value = raw_value.strip()
  347. else:
  348. c_value = ' '.join(raw_value.strip().split())
  349. canonical.append('%s:%s' % (c_name, c_value))
  350. return '\n'.join(sorted(canonical))
  351. def signed_headers(self, headers_to_sign):
  352. l = ['%s' % n.lower().strip() for n in headers_to_sign]
  353. l = sorted(l)
  354. return ';'.join(l)
  355. def canonical_uri(self, http_request):
  356. path = http_request.auth_path
  357. # Normalize the path
  358. # in windows normpath('/') will be '\\' so we chane it back to '/'
  359. normalized = posixpath.normpath(path).replace('\\', '/')
  360. # Then urlencode whatever's left.
  361. encoded = urllib.parse.quote(normalized)
  362. if len(path) > 1 and path.endswith('/'):
  363. encoded += '/'
  364. return encoded
  365. def payload(self, http_request):
  366. body = http_request.body
  367. # If the body is a file like object, we can use
  368. # boto.utils.compute_hash, which will avoid reading
  369. # the entire body into memory.
  370. if hasattr(body, 'seek') and hasattr(body, 'read'):
  371. return boto.utils.compute_hash(body, hash_algorithm=sha256)[0]
  372. elif not isinstance(body, bytes):
  373. body = body.encode('utf-8')
  374. return sha256(body).hexdigest()
  375. def canonical_request(self, http_request):
  376. cr = [http_request.method.upper()]
  377. cr.append(self.canonical_uri(http_request))
  378. cr.append(self.canonical_query_string(http_request))
  379. headers_to_sign = self.headers_to_sign(http_request)
  380. cr.append(self.canonical_headers(headers_to_sign) + '\n')
  381. cr.append(self.signed_headers(headers_to_sign))
  382. cr.append(self.payload(http_request))
  383. return '\n'.join(cr)
  384. def scope(self, http_request):
  385. scope = [self._provider.access_key]
  386. scope.append(http_request.timestamp)
  387. scope.append(http_request.region_name)
  388. scope.append(http_request.service_name)
  389. scope.append('aws4_request')
  390. return '/'.join(scope)
  391. def split_host_parts(self, host):
  392. return host.split('.')
  393. def determine_region_name(self, host):
  394. parts = self.split_host_parts(host)
  395. if self.region_name is not None:
  396. region_name = self.region_name
  397. elif len(parts) > 1:
  398. if parts[1] == 'us-gov':
  399. region_name = 'us-gov-west-1'
  400. else:
  401. if len(parts) == 3:
  402. region_name = 'us-east-1'
  403. else:
  404. region_name = parts[1]
  405. else:
  406. region_name = parts[0]
  407. return region_name
  408. def determine_service_name(self, host):
  409. parts = self.split_host_parts(host)
  410. if self.service_name is not None:
  411. service_name = self.service_name
  412. else:
  413. service_name = parts[0]
  414. return service_name
  415. def credential_scope(self, http_request):
  416. scope = []
  417. http_request.timestamp = http_request.headers['X-Amz-Date'][0:8]
  418. scope.append(http_request.timestamp)
  419. # The service_name and region_name either come from:
  420. # * The service_name/region_name attrs or (if these values are None)
  421. # * parsed from the endpoint <service>.<region>.amazonaws.com.
  422. region_name = self.determine_region_name(http_request.host)
  423. service_name = self.determine_service_name(http_request.host)
  424. http_request.service_name = service_name
  425. http_request.region_name = region_name
  426. scope.append(http_request.region_name)
  427. scope.append(http_request.service_name)
  428. scope.append('aws4_request')
  429. return '/'.join(scope)
  430. def string_to_sign(self, http_request, canonical_request):
  431. """
  432. Return the canonical StringToSign as well as a dict
  433. containing the original version of all headers that
  434. were included in the StringToSign.
  435. """
  436. sts = ['AWS4-HMAC-SHA256']
  437. sts.append(http_request.headers['X-Amz-Date'])
  438. sts.append(self.credential_scope(http_request))
  439. sts.append(sha256(canonical_request.encode('utf-8')).hexdigest())
  440. return '\n'.join(sts)
  441. def signature(self, http_request, string_to_sign):
  442. key = self._provider.secret_key
  443. k_date = self._sign(('AWS4' + key).encode('utf-8'),
  444. http_request.timestamp)
  445. k_region = self._sign(k_date, http_request.region_name)
  446. k_service = self._sign(k_region, http_request.service_name)
  447. k_signing = self._sign(k_service, 'aws4_request')
  448. return self._sign(k_signing, string_to_sign, hex=True)
  449. def add_auth(self, req, **kwargs):
  450. """
  451. Add AWS4 authentication to a request.
  452. :type req: :class`boto.connection.HTTPRequest`
  453. :param req: The HTTPRequest object.
  454. """
  455. # This could be a retry. Make sure the previous
  456. # authorization header is removed first.
  457. if 'X-Amzn-Authorization' in req.headers:
  458. del req.headers['X-Amzn-Authorization']
  459. now = datetime.datetime.utcnow()
  460. req.headers['X-Amz-Date'] = now.strftime('%Y%m%dT%H%M%SZ')
  461. if self._provider.security_token:
  462. req.headers['X-Amz-Security-Token'] = self._provider.security_token
  463. qs = self.query_string(req)
  464. qs_to_post = qs
  465. # We do not want to include any params that were mangled into
  466. # the params if performing s3-sigv4 since it does not
  467. # belong in the body of a post for some requests. Mangled
  468. # refers to items in the query string URL being added to the
  469. # http response params. However, these params get added to
  470. # the body of the request, but the query string URL does not
  471. # belong in the body of the request. ``unmangled_resp`` is the
  472. # response that happened prior to the mangling. This ``unmangled_req``
  473. # kwarg will only appear for s3-sigv4.
  474. if 'unmangled_req' in kwargs:
  475. qs_to_post = self.query_string(kwargs['unmangled_req'])
  476. if qs_to_post and req.method == 'POST':
  477. # Stash request parameters into post body
  478. # before we generate the signature.
  479. req.body = qs_to_post
  480. req.headers['Content-Type'] = 'application/x-www-form-urlencoded; charset=UTF-8'
  481. req.headers['Content-Length'] = str(len(req.body))
  482. else:
  483. # Safe to modify req.path here since
  484. # the signature will use req.auth_path.
  485. req.path = req.path.split('?')[0]
  486. if qs:
  487. # Don't insert the '?' unless there's actually a query string
  488. req.path = req.path + '?' + qs
  489. canonical_request = self.canonical_request(req)
  490. boto.log.debug('CanonicalRequest:\n%s' % canonical_request)
  491. string_to_sign = self.string_to_sign(req, canonical_request)
  492. boto.log.debug('StringToSign:\n%s' % string_to_sign)
  493. signature = self.signature(req, string_to_sign)
  494. boto.log.debug('Signature:\n%s' % signature)
  495. headers_to_sign = self.headers_to_sign(req)
  496. l = ['AWS4-HMAC-SHA256 Credential=%s' % self.scope(req)]
  497. l.append('SignedHeaders=%s' % self.signed_headers(headers_to_sign))
  498. l.append('Signature=%s' % signature)
  499. req.headers['Authorization'] = ','.join(l)
  500. class S3HmacAuthV4Handler(HmacAuthV4Handler, AuthHandler):
  501. """
  502. Implements a variant of Version 4 HMAC authorization specific to S3.
  503. """
  504. capability = ['hmac-v4-s3']
  505. def __init__(self, *args, **kwargs):
  506. super(S3HmacAuthV4Handler, self).__init__(*args, **kwargs)
  507. if self.region_name:
  508. self.region_name = self.clean_region_name(self.region_name)
  509. def clean_region_name(self, region_name):
  510. if region_name.startswith('s3-'):
  511. return region_name[3:]
  512. return region_name
  513. def canonical_uri(self, http_request):
  514. # S3 does **NOT** do path normalization that SigV4 typically does.
  515. # Urlencode the path, **NOT** ``auth_path`` (because vhosting).
  516. path = urllib.parse.urlparse(http_request.path)
  517. # Because some quoting may have already been applied, let's back it out.
  518. unquoted = urllib.parse.unquote(path.path)
  519. # Requote, this time addressing all characters.
  520. encoded = urllib.parse.quote(unquoted, safe='/~')
  521. return encoded
  522. def canonical_query_string(self, http_request):
  523. # Note that we just do not return an empty string for
  524. # POST request. Query strings in url are included in canonical
  525. # query string.
  526. l = []
  527. for param in sorted(http_request.params):
  528. value = boto.utils.get_utf8_value(http_request.params[param])
  529. l.append('%s=%s' % (urllib.parse.quote(param, safe='-_.~'),
  530. urllib.parse.quote(value, safe='-_.~')))
  531. return '&'.join(l)
  532. def host_header(self, host, http_request):
  533. port = http_request.port
  534. secure = http_request.protocol == 'https'
  535. if ((port == 80 and not secure) or (port == 443 and secure)):
  536. return http_request.host
  537. return '%s:%s' % (http_request.host, port)
  538. def headers_to_sign(self, http_request):
  539. """
  540. Select the headers from the request that need to be included
  541. in the StringToSign.
  542. """
  543. host_header_value = self.host_header(self.host, http_request)
  544. headers_to_sign = {'Host': host_header_value}
  545. for name, value in http_request.headers.items():
  546. lname = name.lower()
  547. # Hooray for the only difference! The main SigV4 signer only does
  548. # ``Host`` + ``x-amz-*``. But S3 wants pretty much everything
  549. # signed, except for authorization itself.
  550. if lname not in ['authorization']:
  551. headers_to_sign[name] = value
  552. return headers_to_sign
  553. def determine_region_name(self, host):
  554. # S3's different format(s) of representing region/service from the
  555. # rest of AWS makes this hurt too.
  556. #
  557. # Possible domain formats:
  558. # - s3.amazonaws.com (Classic)
  559. # - s3-us-west-2.amazonaws.com (Specific region)
  560. # - bukkit.s3.amazonaws.com (Vhosted Classic)
  561. # - bukkit.s3-ap-northeast-1.amazonaws.com (Vhosted specific region)
  562. # - s3.cn-north-1.amazonaws.com.cn - (Beijing region)
  563. # - bukkit.s3.cn-north-1.amazonaws.com.cn - (Vhosted Beijing region)
  564. parts = self.split_host_parts(host)
  565. if self.region_name is not None:
  566. region_name = self.region_name
  567. else:
  568. # Classic URLs - s3-us-west-2.amazonaws.com
  569. if len(parts) == 3:
  570. region_name = self.clean_region_name(parts[0])
  571. # Special-case for Classic.
  572. if region_name == 's3':
  573. region_name = 'us-east-1'
  574. else:
  575. # Iterate over the parts in reverse order.
  576. for offset, part in enumerate(reversed(parts)):
  577. part = part.lower()
  578. # Look for the first thing starting with 's3'.
  579. # Until there's a ``.s3`` TLD, we should be OK. :P
  580. if part == 's3':
  581. # If it's by itself, the region is the previous part.
  582. region_name = parts[-offset]
  583. # Unless it's Vhosted classic
  584. if region_name == 'amazonaws':
  585. region_name = 'us-east-1'
  586. break
  587. elif part.startswith('s3-'):
  588. region_name = self.clean_region_name(part)
  589. break
  590. return region_name
  591. def determine_service_name(self, host):
  592. # Should this signing mechanism ever be used for anything else, this
  593. # will fail. Consider utilizing the logic from the parent class should
  594. # you find yourself here.
  595. return 's3'
  596. def mangle_path_and_params(self, req):
  597. """
  598. Returns a copy of the request object with fixed ``auth_path/params``
  599. attributes from the original.
  600. """
  601. modified_req = copy.copy(req)
  602. # Unlike the most other services, in S3, ``req.params`` isn't the only
  603. # source of query string parameters.
  604. # Because of the ``query_args``, we may already have a query string
  605. # **ON** the ``path/auth_path``.
  606. # Rip them apart, so the ``auth_path/params`` can be signed
  607. # appropriately.
  608. parsed_path = urllib.parse.urlparse(modified_req.auth_path)
  609. modified_req.auth_path = parsed_path.path
  610. if modified_req.params is None:
  611. modified_req.params = {}
  612. else:
  613. # To keep the original request object untouched. We must make
  614. # a copy of the params dictionary. Because the copy of the
  615. # original request directly refers to the params dictionary
  616. # of the original request.
  617. copy_params = req.params.copy()
  618. modified_req.params = copy_params
  619. raw_qs = parsed_path.query
  620. existing_qs = parse_qs_safe(
  621. raw_qs,
  622. keep_blank_values=True
  623. )
  624. # ``parse_qs`` will return lists. Don't do that unless there's a real,
  625. # live list provided.
  626. for key, value in existing_qs.items():
  627. if isinstance(value, (list, tuple)):
  628. if len(value) == 1:
  629. existing_qs[key] = value[0]
  630. modified_req.params.update(existing_qs)
  631. return modified_req
  632. def payload(self, http_request):
  633. if http_request.headers.get('x-amz-content-sha256'):
  634. return http_request.headers['x-amz-content-sha256']
  635. return super(S3HmacAuthV4Handler, self).payload(http_request)
  636. def add_auth(self, req, **kwargs):
  637. if 'x-amz-content-sha256' not in req.headers:
  638. if '_sha256' in req.headers:
  639. req.headers['x-amz-content-sha256'] = req.headers.pop('_sha256')
  640. else:
  641. req.headers['x-amz-content-sha256'] = self.payload(req)
  642. updated_req = self.mangle_path_and_params(req)
  643. return super(S3HmacAuthV4Handler, self).add_auth(updated_req,
  644. unmangled_req=req,
  645. **kwargs)
  646. def presign(self, req, expires, iso_date=None):
  647. """
  648. Presign a request using SigV4 query params. Takes in an HTTP request
  649. and an expiration time in seconds and returns a URL.
  650. http://docs.aws.amazon.com/AmazonS3/latest/API/sigv4-query-string-auth.html
  651. """
  652. if iso_date is None:
  653. iso_date = datetime.datetime.utcnow().strftime('%Y%m%dT%H%M%SZ')
  654. region = self.determine_region_name(req.host)
  655. service = self.determine_service_name(req.host)
  656. params = {
  657. 'X-Amz-Algorithm': 'AWS4-HMAC-SHA256',
  658. 'X-Amz-Credential': '%s/%s/%s/%s/aws4_request' % (
  659. self._provider.access_key,
  660. iso_date[:8],
  661. region,
  662. service
  663. ),
  664. 'X-Amz-Date': iso_date,
  665. 'X-Amz-Expires': expires,
  666. 'X-Amz-SignedHeaders': 'host'
  667. }
  668. if self._provider.security_token:
  669. params['X-Amz-Security-Token'] = self._provider.security_token
  670. headers_to_sign = self.headers_to_sign(req)
  671. l = sorted(['%s' % n.lower().strip() for n in headers_to_sign])
  672. params['X-Amz-SignedHeaders'] = ';'.join(l)
  673. req.params.update(params)
  674. cr = self.canonical_request(req)
  675. # We need to replace the payload SHA with a constant
  676. cr = '\n'.join(cr.split('\n')[:-1]) + '\nUNSIGNED-PAYLOAD'
  677. # Date header is expected for string_to_sign, but unused otherwise
  678. req.headers['X-Amz-Date'] = iso_date
  679. sts = self.string_to_sign(req, cr)
  680. signature = self.signature(req, sts)
  681. # Add signature to params now that we have it
  682. req.params['X-Amz-Signature'] = signature
  683. return '%s://%s%s?%s' % (req.protocol, req.host, req.path,
  684. urllib.parse.urlencode(req.params))
  685. class STSAnonHandler(AuthHandler):
  686. """
  687. Provides pure query construction (no actual signing).
  688. Used for making anonymous STS request for operations like
  689. ``assume_role_with_web_identity``.
  690. """
  691. capability = ['sts-anon']
  692. def _escape_value(self, value):
  693. # This is changed from a previous version because this string is
  694. # being passed to the query string and query strings must
  695. # be url encoded. In particular STS requires the saml_response to
  696. # be urlencoded when calling assume_role_with_saml.
  697. return urllib.parse.quote(value)
  698. def _build_query_string(self, params):
  699. keys = list(params.keys())
  700. keys.sort(key=lambda x: x.lower())
  701. pairs = []
  702. for key in keys:
  703. val = boto.utils.get_utf8_value(params[key])
  704. pairs.append(key + '=' + self._escape_value(val.decode('utf-8')))
  705. return '&'.join(pairs)
  706. def add_auth(self, http_request, **kwargs):
  707. headers = http_request.headers
  708. qs = self._build_query_string(
  709. http_request.params
  710. )
  711. boto.log.debug('query_string in body: %s' % qs)
  712. headers['Content-Type'] = 'application/x-www-form-urlencoded'
  713. # This will be a POST so the query string should go into the body
  714. # as opposed to being in the uri
  715. http_request.body = qs
  716. class QuerySignatureHelper(HmacKeys):
  717. """
  718. Helper for Query signature based Auth handler.
  719. Concrete sub class need to implement _calc_sigature method.
  720. """
  721. def add_auth(self, http_request, **kwargs):
  722. headers = http_request.headers
  723. params = http_request.params
  724. params['AWSAccessKeyId'] = self._provider.access_key
  725. params['SignatureVersion'] = self.SignatureVersion
  726. params['Timestamp'] = boto.utils.get_ts()
  727. qs, signature = self._calc_signature(
  728. http_request.params, http_request.method,
  729. http_request.auth_path, http_request.host)
  730. boto.log.debug('query_string: %s Signature: %s' % (qs, signature))
  731. if http_request.method == 'POST':
  732. headers['Content-Type'] = 'application/x-www-form-urlencoded; charset=UTF-8'
  733. http_request.body = qs + '&Signature=' + urllib.parse.quote_plus(signature)
  734. http_request.headers['Content-Length'] = str(len(http_request.body))
  735. else:
  736. http_request.body = ''
  737. # if this is a retried request, the qs from the previous try will
  738. # already be there, we need to get rid of that and rebuild it
  739. http_request.path = http_request.path.split('?')[0]
  740. http_request.path = (http_request.path + '?' + qs +
  741. '&Signature=' + urllib.parse.quote_plus(signature))
  742. class QuerySignatureV0AuthHandler(QuerySignatureHelper, AuthHandler):
  743. """Provides Signature V0 Signing"""
  744. SignatureVersion = 0
  745. capability = ['sign-v0']
  746. def _calc_signature(self, params, *args):
  747. boto.log.debug('using _calc_signature_0')
  748. hmac = self._get_hmac()
  749. s = params['Action'] + params['Timestamp']
  750. hmac.update(s.encode('utf-8'))
  751. keys = params.keys()
  752. keys.sort(cmp=lambda x, y: cmp(x.lower(), y.lower()))
  753. pairs = []
  754. for key in keys:
  755. val = boto.utils.get_utf8_value(params[key])
  756. pairs.append(key + '=' + urllib.parse.quote(val))
  757. qs = '&'.join(pairs)
  758. return (qs, base64.b64encode(hmac.digest()))
  759. class QuerySignatureV1AuthHandler(QuerySignatureHelper, AuthHandler):
  760. """
  761. Provides Query Signature V1 Authentication.
  762. """
  763. SignatureVersion = 1
  764. capability = ['sign-v1', 'mturk']
  765. def __init__(self, *args, **kw):
  766. QuerySignatureHelper.__init__(self, *args, **kw)
  767. AuthHandler.__init__(self, *args, **kw)
  768. self._hmac_256 = None
  769. def _calc_signature(self, params, *args):
  770. boto.log.debug('using _calc_signature_1')
  771. hmac = self._get_hmac()
  772. keys = list(params.keys())
  773. keys.sort(key=lambda x: x.lower())
  774. pairs = []
  775. for key in keys:
  776. hmac.update(key.encode('utf-8'))
  777. val = boto.utils.get_utf8_value(params[key])
  778. hmac.update(val)
  779. pairs.append(key + '=' + urllib.parse.quote(val))
  780. qs = '&'.join(pairs)
  781. return (qs, base64.b64encode(hmac.digest()))
  782. class QuerySignatureV2AuthHandler(QuerySignatureHelper, AuthHandler):
  783. """Provides Query Signature V2 Authentication."""
  784. SignatureVersion = 2
  785. capability = ['sign-v2', 'ec2', 'ec2', 'emr', 'fps', 'ecs',
  786. 'sdb', 'iam', 'rds', 'sns', 'sqs', 'cloudformation']
  787. def _calc_signature(self, params, verb, path, server_name):
  788. boto.log.debug('using _calc_signature_2')
  789. string_to_sign = '%s\n%s\n%s\n' % (verb, server_name.lower(), path)
  790. hmac = self._get_hmac()
  791. params['SignatureMethod'] = self.algorithm()
  792. if self._provider.security_token:
  793. params['SecurityToken'] = self._provider.security_token
  794. keys = sorted(params.keys())
  795. pairs = []
  796. for key in keys:
  797. val = boto.utils.get_utf8_value(params[key])
  798. pairs.append(urllib.parse.quote(key, safe='') + '=' +
  799. urllib.parse.quote(val, safe='-_~'))
  800. qs = '&'.join(pairs)
  801. boto.log.debug('query string: %s' % qs)
  802. string_to_sign += qs
  803. boto.log.debug('string_to_sign: %s' % string_to_sign)
  804. hmac.update(string_to_sign.encode('utf-8'))
  805. b64 = base64.b64encode(hmac.digest())
  806. boto.log.debug('len(b64)=%d' % len(b64))
  807. boto.log.debug('base64 encoded digest: %s' % b64)
  808. return (qs, b64)
  809. class POSTPathQSV2AuthHandler(QuerySignatureV2AuthHandler, AuthHandler):
  810. """
  811. Query Signature V2 Authentication relocating signed query
  812. into the path and allowing POST requests with Content-Types.
  813. """
  814. capability = ['mws']
  815. def add_auth(self, req, **kwargs):
  816. req.params['AWSAccessKeyId'] = self._provider.access_key
  817. req.params['SignatureVersion'] = self.SignatureVersion
  818. req.params['Timestamp'] = boto.utils.get_ts()
  819. qs, signature = self._calc_signature(req.params, req.method,
  820. req.auth_path, req.host)
  821. boto.log.debug('query_string: %s Signature: %s' % (qs, signature))
  822. if req.method == 'POST':
  823. req.headers['Content-Length'] = str(len(req.body))
  824. req.headers['Content-Type'] = req.headers.get('Content-Type',
  825. 'text/plain')
  826. else:
  827. req.body = ''
  828. # if this is a retried req, the qs from the previous try will
  829. # already be there, we need to get rid of that and rebuild it
  830. req.path = req.path.split('?')[0]
  831. req.path = (req.path + '?' + qs +
  832. '&Signature=' + urllib.parse.quote_plus(signature))
  833. def get_auth_handler(host, config, provider, requested_capability=None):
  834. """Finds an AuthHandler that is ready to authenticate.
  835. Lists through all the registered AuthHandlers to find one that is willing
  836. to handle for the requested capabilities, config and provider.
  837. :type host: string
  838. :param host: The name of the host
  839. :type config:
  840. :param config:
  841. :type provider:
  842. :param provider:
  843. Returns:
  844. An implementation of AuthHandler.
  845. Raises:
  846. boto.exception.NoAuthHandlerFound
  847. """
  848. ready_handlers = []
  849. auth_handlers = boto.plugin.get_plugin(AuthHandler, requested_capability)
  850. for handler in auth_handlers:
  851. try:
  852. ready_handlers.append(handler(host, config, provider))
  853. except boto.auth_handler.NotReadyToAuthenticate:
  854. pass
  855. if not ready_handlers:
  856. checked_handlers = auth_handlers
  857. names = [handler.__name__ for handler in checked_handlers]
  858. raise boto.exception.NoAuthHandlerFound(
  859. 'No handler was ready to authenticate. %d handlers were checked.'
  860. ' %s '
  861. 'Check your credentials' % (len(names), str(names)))
  862. # We select the last ready auth handler that was loaded, to allow users to
  863. # customize how auth works in environments where there are shared boto
  864. # config files (e.g., /etc/boto.cfg and ~/.boto): The more general,
  865. # system-wide shared configs should be loaded first, and the user's
  866. # customizations loaded last. That way, for example, the system-wide
  867. # config might include a plugin_directory that includes a service account
  868. # auth plugin shared by all users of a Google Compute Engine instance
  869. # (allowing sharing of non-user data between various services), and the
  870. # user could override this with a .boto config that includes user-specific
  871. # credentials (for access to user data).
  872. return ready_handlers[-1]
  873. def detect_potential_sigv4(func):
  874. def _wrapper(self):
  875. if os.environ.get('EC2_USE_SIGV4', False):
  876. return ['hmac-v4']
  877. if boto.config.get('ec2', 'use-sigv4', False):
  878. return ['hmac-v4']
  879. if hasattr(self, 'region'):
  880. # If you're making changes here, you should also check
  881. # ``boto/iam/connection.py``, as several things there are also
  882. # endpoint-related.
  883. if getattr(self.region, 'endpoint', ''):
  884. for test in SIGV4_DETECT:
  885. if test in self.region.endpoint:
  886. return ['hmac-v4']
  887. return func(self)
  888. return _wrapper
  889. def detect_potential_s3sigv4(func):
  890. def _wrapper(self):
  891. if os.environ.get('S3_USE_SIGV4', False):
  892. return ['hmac-v4-s3']
  893. if boto.config.get('s3', 'use-sigv4', False):
  894. return ['hmac-v4-s3']
  895. if not hasattr(self, 'host'):
  896. return func(self)
  897. # Keep the old explicit logic in case somebody was adding to the list.
  898. for test in SIGV4_DETECT:
  899. if test in self.host:
  900. return ['hmac-v4-s3']
  901. # Use default for non-aws hosts. Adding a url scheme is necessary if
  902. # not present for urlparse to properly function.
  903. host = self.host
  904. if not self.host.startswith('http://') or \
  905. self.host.startswith('https://'):
  906. host = 'https://' + host
  907. netloc = urlparse(host).netloc
  908. if not (netloc.endswith('amazonaws.com') or
  909. netloc.endswith('amazonaws.com.cn')):
  910. return func(self)
  911. # Use the default for the global endpoint
  912. if netloc.endswith('s3.amazonaws.com'):
  913. return func(self)
  914. # Use the default for regions that support sigv4 and sigv2
  915. if any(test in self.host for test in S3_AUTH_DETECT):
  916. return func(self)
  917. # Use anonymous if enabled.
  918. if hasattr(self, 'anon') and self.anon:
  919. return func(self)
  920. # Default to sigv4 for aws hosts outside of regions that are known
  921. # to support sigv2
  922. return ['hmac-v4-s3']
  923. return _wrapper