bce_http_client.py 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294
  1. # Copyright 2014 Baidu, Inc.
  2. #
  3. # Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file
  4. # except in compliance with the License. You may obtain a copy of the License at
  5. #
  6. # http://www.apache.org/licenses/LICENSE-2.0
  7. #
  8. # Unless required by applicable law or agreed to in writing, software distributed under the
  9. # License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
  10. # either express or implied. See the License for the specific language governing permissions
  11. # and limitations under the License.
  12. # -*- coding: utf-8 -*-
  13. """
  14. This module provide http request function for bce services.
  15. """
  16. from future.utils import iteritems, iterkeys, itervalues
  17. from builtins import str, bytes
  18. import logging
  19. import http.client
  20. import sys
  21. import time
  22. import traceback
  23. import baidubce
  24. from baidubce import compat
  25. from baidubce import utils
  26. from baidubce.bce_response import BceResponse
  27. from baidubce.exception import BceHttpClientError
  28. from baidubce.exception import BceServerError
  29. from baidubce.exception import BceClientError
  30. from baidubce.http import http_headers
  31. try:
  32. from urllib.parse import urlparse
  33. except ImportError:
  34. from urlparse import urlparse
  35. _logger = logging.getLogger(__name__)
  36. def _get_connection(protocol, host, port, connection_timeout_in_millis, proxy_host=None, proxy_port=None):
  37. """
  38. :param protocol
  39. :type protocol: baidubce.protocol.Protocol
  40. :param endpoint
  41. :type endpoint: str
  42. :param connection_timeout_in_millis
  43. :type connection_timeout_in_millis int
  44. """
  45. host = compat.convert_to_string(host)
  46. if protocol.name == baidubce.protocol.HTTP.name:
  47. if proxy_host and proxy_port:
  48. _logger.debug('Using proxy host: %s, port: %d' % (proxy_host, proxy_port))
  49. conn = http.client.HTTPConnection(host=proxy_host, port=proxy_port,
  50. timeout=connection_timeout_in_millis / 1000)
  51. conn.set_tunnel(host, port)
  52. return conn
  53. return http.client.HTTPConnection(
  54. host=host, port=port, timeout=connection_timeout_in_millis / 1000)
  55. elif protocol.name == baidubce.protocol.HTTPS.name:
  56. if proxy_host and proxy_port:
  57. _logger.debug('Using proxy host: %s, port: %d' % (host, port))
  58. conn = http.client.HTTPSConnection(host=proxy_host, port=proxy_port,
  59. timeout=connection_timeout_in_millis / 1000)
  60. conn.set_tunnel(host, port)
  61. return conn
  62. return http.client.HTTPSConnection(
  63. host=host, port=port, timeout=connection_timeout_in_millis / 1000)
  64. else:
  65. raise ValueError(
  66. 'Invalid protocol: %s, either HTTP or HTTPS is expected.' % protocol)
  67. def _send_http_request(conn, http_method, uri, headers, body, send_buf_size):
  68. # putrequest() need that http_method and uri is Ascii on Py2 and unicode \
  69. # on Py3
  70. http_method = compat.convert_to_string(http_method)
  71. uri = compat.convert_to_string(uri)
  72. conn.putrequest(http_method, uri, skip_host=True, skip_accept_encoding=True)
  73. for k, v in iteritems(headers):
  74. k = utils.convert_to_standard_string(k)
  75. v = utils.convert_to_standard_string(v)
  76. conn.putheader(k, v)
  77. conn.endheaders()
  78. if body:
  79. if isinstance(body, (bytes, str)):
  80. conn.send(body)
  81. else:
  82. total = int(headers[http_headers.CONTENT_LENGTH])
  83. sent = 0
  84. while sent < total:
  85. size = total - sent
  86. if size > send_buf_size:
  87. size = send_buf_size
  88. buf = body.read(size)
  89. if not buf:
  90. raise BceClientError(
  91. 'Insufficient data, only %d bytes available while %s is %d' % (
  92. sent, http_headers.CONTENT_LENGTH, total))
  93. conn.send(buf)
  94. sent += len(buf)
  95. return conn.getresponse()
  96. def check_headers(headers):
  97. """
  98. check value in headers, if \n in value, raise
  99. :param headers:
  100. :return:
  101. """
  102. for k, v in iteritems(headers):
  103. if isinstance(v, (bytes, str)) and \
  104. b'\n' in compat.convert_to_bytes(v):
  105. raise BceClientError(r'There should not be any "\n" in header[%s]:%s' % (k, v))
  106. def send_request(
  107. config,
  108. sign_function,
  109. response_handler_functions,
  110. http_method, path, body, headers, params, use_backup_endpoint=False):
  111. """
  112. Send request to BCE services.
  113. :param config
  114. :type config: baidubce.BceClientConfiguration
  115. :param sign_function:
  116. :param response_handler_functions:
  117. :type response_handler_functions: list
  118. :param request:
  119. :type request: baidubce.internal.InternalRequest
  120. :return:
  121. :rtype: baidubce.BceResponse
  122. """
  123. _logger.debug(b'%s request start: %s %s, %s, %s',
  124. http_method, path, headers, params, body)
  125. headers = headers or {}
  126. user_agent = 'bce-sdk-python/%s/%s/%s' % (
  127. compat.convert_to_string(baidubce.SDK_VERSION), sys.version, sys.platform)
  128. user_agent = user_agent.replace('\n', '')
  129. user_agent = compat.convert_to_bytes(user_agent)
  130. headers[http_headers.USER_AGENT] = user_agent
  131. should_get_new_date = False
  132. if http_headers.BCE_DATE not in headers:
  133. should_get_new_date = True
  134. request_endpoint = config.endpoint
  135. if use_backup_endpoint:
  136. request_endpoint = config.backup_endpoint
  137. headers[http_headers.HOST] = request_endpoint
  138. if isinstance(body, str):
  139. body = body.encode(baidubce.DEFAULT_ENCODING)
  140. if not body:
  141. headers[http_headers.CONTENT_LENGTH] = 0
  142. elif isinstance(body, bytes):
  143. headers[http_headers.CONTENT_LENGTH] = len(body)
  144. elif http_headers.CONTENT_LENGTH not in headers:
  145. raise ValueError(b'No %s is specified.' % http_headers.CONTENT_LENGTH)
  146. # store the offset of fp body
  147. offset = None
  148. if hasattr(body, "tell") and hasattr(body, "seek"):
  149. offset = body.tell()
  150. protocol, host, port = utils.parse_host_port(request_endpoint, config.protocol)
  151. headers[http_headers.HOST] = host
  152. if port != config.protocol.default_port:
  153. headers[http_headers.HOST] += b':' + compat.convert_to_bytes(port)
  154. headers[http_headers.AUTHORIZATION] = sign_function(
  155. config.credentials, http_method, path, headers, params)
  156. encoded_params = utils.get_canonical_querystring(params, False)
  157. if len(encoded_params) > 0:
  158. uri = path + b'?' + encoded_params
  159. else:
  160. uri = path
  161. check_headers(headers)
  162. retries_attempted = 0
  163. errors = []
  164. while True:
  165. conn = None
  166. try:
  167. # restore the offset of fp body when retrying
  168. if should_get_new_date is True:
  169. headers[http_headers.BCE_DATE] = utils.get_canonical_time()
  170. headers[http_headers.AUTHORIZATION] = sign_function(
  171. config.credentials, http_method, path, headers, params)
  172. if retries_attempted > 0 and offset is not None:
  173. body.seek(offset)
  174. conn = _get_connection(protocol, host, port, config.connection_timeout_in_mills,
  175. config.proxy_host, config.proxy_port)
  176. _logger.debug('request args:method=%s, uri=%s, headers=%s,patams=%s, body=%s',
  177. http_method, uri, headers, params, body)
  178. http_response = _send_http_request(
  179. conn, http_method, uri, headers, body, config.send_buf_size)
  180. headers_list = http_response.getheaders()
  181. # on py3 ,values of headers_list is decoded with ios-8859-1 from
  182. # utf-8 binary bytes
  183. # headers_list[*][0] is lowercase on py2
  184. # headers_list[*][0] is raw value py3
  185. if compat.PY3 and isinstance(headers_list, list):
  186. temp_heads = []
  187. for k, v in headers_list:
  188. k = k.encode('latin-1').decode('utf-8')
  189. v = v.encode('latin-1').decode('utf-8')
  190. k = k.lower()
  191. temp_heads.append((k, v))
  192. headers_list = temp_heads
  193. _logger.debug(
  194. 'request return: status=%d, headers=%s' % (http_response.status, headers_list))
  195. response = BceResponse()
  196. if config.under_line_headers:
  197. response.set_metadata_from_headers(dict(headers_list))
  198. else:
  199. response.set_metadata_from_headers_no_underlined(dict(headers_list))
  200. if config.auto_follow_redirect:
  201. if http_method == b'GET' and 300 <= http_response.status < 400:
  202. headers_map = {k: v for k, v in headers_list}
  203. if 'location' in headers_map:
  204. try:
  205. http_response.read()
  206. except Exception:
  207. _logger.info("ignore read response body error")
  208. try:
  209. http_response.close()
  210. except Exception:
  211. _logger.info("ignore close response connection error")
  212. location = headers_map.get('location')
  213. _logger.debug('request auto follow redirect location is %s', location)
  214. parsed_url = urlparse(location)
  215. redirect_conn = None
  216. if protocol.name == baidubce.protocol.HTTP.name:
  217. redirect_conn = http.client.HTTPConnection(parsed_url.netloc,
  218. timeout=config.connection_timeout_in_mills / 1000)
  219. elif protocol.name == baidubce.protocol.HTTPS.name:
  220. redirect_conn = http.client.HTTPSConnection(parsed_url.netloc,
  221. timeout=config.connection_timeout_in_mills / 1000)
  222. else:
  223. raise ValueError('Invalid protocol: %s, either HTTP or HTTPS is expected.' % protocol)
  224. redirect_conn.putrequest("GET", parsed_url.path + "?" + parsed_url.query,
  225. skip_host=True, skip_accept_encoding=True)
  226. redirect_conn.putheader("Host", parsed_url.netloc)
  227. redirect_conn.endheaders()
  228. http_response = redirect_conn.getresponse()
  229. for handler_function in response_handler_functions:
  230. if handler_function(http_response, response):
  231. break
  232. return response
  233. except Exception as e:
  234. if conn is not None:
  235. conn.close()
  236. # insert ">>>>" before all trace back lines and then save it
  237. errors.append('\n'.join('>>>>' + line for line in traceback.format_exc().splitlines()))
  238. if isinstance(e, BceServerError):
  239. request_id = e.request_id
  240. status_code = e.status_code
  241. code = e.code
  242. else:
  243. request_id = None
  244. status_code = None
  245. code = None
  246. if config.retry_policy.should_retry(e, retries_attempted):
  247. delay_in_millis = config.retry_policy.get_delay_before_next_retry_in_millis(
  248. e, retries_attempted)
  249. time.sleep(delay_in_millis / 1000.0)
  250. else:
  251. raise BceHttpClientError('Unable to execute HTTP request. Retried %d times. '
  252. 'All trace backs:\n%s' % (retries_attempted,
  253. '\n'.join(errors)), e,
  254. status_code, code,
  255. request_id=request_id)
  256. retries_attempted += 1