Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.8/site-packages/botocore/awsrequest.py: 26%

277 statements  

« prev     ^ index     » next       coverage.py v7.2.2, created at 2023-03-26 06:03 +0000

1# Copyright (c) 2012-2013 Mitch Garnaat http://garnaat.org/ 

2# Copyright 2012-2014 Amazon.com, Inc. or its affiliates. All Rights Reserved. 

3# 

4# Licensed under the Apache License, Version 2.0 (the "License"). You 

5# may not use this file except in compliance with the License. A copy of 

6# the License is located at 

7# 

8# http://aws.amazon.com/apache2.0/ 

9# 

10# or in the "license" file accompanying this file. This file is 

11# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF 

12# ANY KIND, either express or implied. See the License for the specific 

13# language governing permissions and limitations under the License. 

14import functools 

15import logging 

16from collections.abc import Mapping 

17 

18import urllib3.util 

19from urllib3.connection import HTTPConnection, VerifiedHTTPSConnection 

20from urllib3.connectionpool import HTTPConnectionPool, HTTPSConnectionPool 

21 

22import botocore.utils 

23from botocore.compat import ( 

24 HTTPHeaders, 

25 HTTPResponse, 

26 MutableMapping, 

27 urlencode, 

28 urlparse, 

29 urlsplit, 

30 urlunsplit, 

31) 

32from botocore.exceptions import UnseekableStreamError 

33 

34logger = logging.getLogger(__name__) 

35 

36 

37class AWSHTTPResponse(HTTPResponse): 

38 # The *args, **kwargs is used because the args are slightly 

39 # different in py2.6 than in py2.7/py3. 

40 def __init__(self, *args, **kwargs): 

41 self._status_tuple = kwargs.pop('status_tuple') 

42 HTTPResponse.__init__(self, *args, **kwargs) 

43 

44 def _read_status(self): 

45 if self._status_tuple is not None: 

46 status_tuple = self._status_tuple 

47 self._status_tuple = None 

48 return status_tuple 

49 else: 

50 return HTTPResponse._read_status(self) 

51 

52 

53class AWSConnection: 

54 """Mixin for HTTPConnection that supports Expect 100-continue. 

55 

56 This when mixed with a subclass of httplib.HTTPConnection (though 

57 technically we subclass from urllib3, which subclasses 

58 httplib.HTTPConnection) and we only override this class to support Expect 

59 100-continue, which we need for S3. As far as I can tell, this is 

60 general purpose enough to not be specific to S3, but I'm being 

61 tentative and keeping it in botocore because I've only tested 

62 this against AWS services. 

63 

64 """ 

65 

66 def __init__(self, *args, **kwargs): 

67 super().__init__(*args, **kwargs) 

68 self._original_response_cls = self.response_class 

69 # We'd ideally hook into httplib's states, but they're all 

70 # __mangled_vars so we use our own state var. This variable is set 

71 # when we receive an early response from the server. If this value is 

72 # set to True, any calls to send() are noops. This value is reset to 

73 # false every time _send_request is called. This is to workaround the 

74 # fact that py2.6 (and only py2.6) has a separate send() call for the 

75 # body in _send_request, as opposed to endheaders(), which is where the 

76 # body is sent in all versions > 2.6. 

77 self._response_received = False 

78 self._expect_header_set = False 

79 

80 def close(self): 

81 super().close() 

82 # Reset all of our instance state we were tracking. 

83 self._response_received = False 

84 self._expect_header_set = False 

85 self.response_class = self._original_response_cls 

86 

87 def _send_request(self, method, url, body, headers, *args, **kwargs): 

88 self._response_received = False 

89 if headers.get('Expect', b'') == b'100-continue': 

90 self._expect_header_set = True 

91 else: 

92 self._expect_header_set = False 

93 self.response_class = self._original_response_cls 

94 rval = super()._send_request( 

95 method, url, body, headers, *args, **kwargs 

96 ) 

97 self._expect_header_set = False 

98 return rval 

99 

100 def _convert_to_bytes(self, mixed_buffer): 

101 # Take a list of mixed str/bytes and convert it 

102 # all into a single bytestring. 

103 # Any str will be encoded as utf-8. 

104 bytes_buffer = [] 

105 for chunk in mixed_buffer: 

106 if isinstance(chunk, str): 

107 bytes_buffer.append(chunk.encode('utf-8')) 

108 else: 

109 bytes_buffer.append(chunk) 

110 msg = b"\r\n".join(bytes_buffer) 

111 return msg 

112 

113 def _send_output(self, message_body=None, *args, **kwargs): 

114 self._buffer.extend((b"", b"")) 

115 msg = self._convert_to_bytes(self._buffer) 

116 del self._buffer[:] 

117 # If msg and message_body are sent in a single send() call, 

118 # it will avoid performance problems caused by the interaction 

119 # between delayed ack and the Nagle algorithm. 

120 if isinstance(message_body, bytes): 

121 msg += message_body 

122 message_body = None 

123 self.send(msg) 

124 if self._expect_header_set: 

125 # This is our custom behavior. If the Expect header was 

126 # set, it will trigger this custom behavior. 

127 logger.debug("Waiting for 100 Continue response.") 

128 # Wait for 1 second for the server to send a response. 

129 if urllib3.util.wait_for_read(self.sock, 1): 

130 self._handle_expect_response(message_body) 

131 return 

132 else: 

133 # From the RFC: 

134 # Because of the presence of older implementations, the 

135 # protocol allows ambiguous situations in which a client may 

136 # send "Expect: 100-continue" without receiving either a 417 

137 # (Expectation Failed) status or a 100 (Continue) status. 

138 # Therefore, when a client sends this header field to an origin 

139 # server (possibly via a proxy) from which it has never seen a 

140 # 100 (Continue) status, the client SHOULD NOT wait for an 

141 # indefinite period before sending the request body. 

142 logger.debug( 

143 "No response seen from server, continuing to " 

144 "send the response body." 

145 ) 

146 if message_body is not None: 

147 # message_body was not a string (i.e. it is a file), and 

148 # we must run the risk of Nagle. 

149 self.send(message_body) 

150 

151 def _consume_headers(self, fp): 

152 # Most servers (including S3) will just return 

153 # the CLRF after the 100 continue response. However, 

154 # some servers (I've specifically seen this for squid when 

155 # used as a straight HTTP proxy) will also inject a 

156 # Connection: keep-alive header. To account for this 

157 # we'll read until we read '\r\n', and ignore any headers 

158 # that come immediately after the 100 continue response. 

159 current = None 

160 while current != b'\r\n': 

161 current = fp.readline() 

162 

163 def _handle_expect_response(self, message_body): 

164 # This is called when we sent the request headers containing 

165 # an Expect: 100-continue header and received a response. 

166 # We now need to figure out what to do. 

167 fp = self.sock.makefile('rb', 0) 

168 try: 

169 maybe_status_line = fp.readline() 

170 parts = maybe_status_line.split(None, 2) 

171 if self._is_100_continue_status(maybe_status_line): 

172 self._consume_headers(fp) 

173 logger.debug( 

174 "100 Continue response seen, now sending request body." 

175 ) 

176 self._send_message_body(message_body) 

177 elif len(parts) == 3 and parts[0].startswith(b'HTTP/'): 

178 # From the RFC: 

179 # Requirements for HTTP/1.1 origin servers: 

180 # 

181 # - Upon receiving a request which includes an Expect 

182 # request-header field with the "100-continue" 

183 # expectation, an origin server MUST either respond with 

184 # 100 (Continue) status and continue to read from the 

185 # input stream, or respond with a final status code. 

186 # 

187 # So if we don't get a 100 Continue response, then 

188 # whatever the server has sent back is the final response 

189 # and don't send the message_body. 

190 logger.debug( 

191 "Received a non 100 Continue response " 

192 "from the server, NOT sending request body." 

193 ) 

194 status_tuple = ( 

195 parts[0].decode('ascii'), 

196 int(parts[1]), 

197 parts[2].decode('ascii'), 

198 ) 

199 response_class = functools.partial( 

200 AWSHTTPResponse, status_tuple=status_tuple 

201 ) 

202 self.response_class = response_class 

203 self._response_received = True 

204 finally: 

205 fp.close() 

206 

207 def _send_message_body(self, message_body): 

208 if message_body is not None: 

209 self.send(message_body) 

210 

211 def send(self, str): 

212 if self._response_received: 

213 logger.debug( 

214 "send() called, but reseponse already received. " 

215 "Not sending data." 

216 ) 

217 return 

218 return super().send(str) 

219 

220 def _is_100_continue_status(self, maybe_status_line): 

221 parts = maybe_status_line.split(None, 2) 

222 # Check for HTTP/<version> 100 Continue\r\n 

223 return ( 

224 len(parts) >= 3 

225 and parts[0].startswith(b'HTTP/') 

226 and parts[1] == b'100' 

227 ) 

228 

229 

230class AWSHTTPConnection(AWSConnection, HTTPConnection): 

231 """An HTTPConnection that supports 100 Continue behavior.""" 

232 

233 

234class AWSHTTPSConnection(AWSConnection, VerifiedHTTPSConnection): 

235 """An HTTPSConnection that supports 100 Continue behavior.""" 

236 

237 

238class AWSHTTPConnectionPool(HTTPConnectionPool): 

239 ConnectionCls = AWSHTTPConnection 

240 

241 

242class AWSHTTPSConnectionPool(HTTPSConnectionPool): 

243 ConnectionCls = AWSHTTPSConnection 

244 

245 

246def prepare_request_dict( 

247 request_dict, endpoint_url, context=None, user_agent=None 

248): 

249 """ 

250 This method prepares a request dict to be created into an 

251 AWSRequestObject. This prepares the request dict by adding the 

252 url and the user agent to the request dict. 

253 

254 :type request_dict: dict 

255 :param request_dict: The request dict (created from the 

256 ``serialize`` module). 

257 

258 :type user_agent: string 

259 :param user_agent: The user agent to use for this request. 

260 

261 :type endpoint_url: string 

262 :param endpoint_url: The full endpoint url, which contains at least 

263 the scheme, the hostname, and optionally any path components. 

264 """ 

265 r = request_dict 

266 if user_agent is not None: 

267 headers = r['headers'] 

268 headers['User-Agent'] = user_agent 

269 host_prefix = r.get('host_prefix') 

270 url = _urljoin(endpoint_url, r['url_path'], host_prefix) 

271 if r['query_string']: 

272 # NOTE: This is to avoid circular import with utils. This is being 

273 # done to avoid moving classes to different modules as to not cause 

274 # breaking chainges. 

275 percent_encode_sequence = botocore.utils.percent_encode_sequence 

276 encoded_query_string = percent_encode_sequence(r['query_string']) 

277 if '?' not in url: 

278 url += '?%s' % encoded_query_string 

279 else: 

280 url += '&%s' % encoded_query_string 

281 r['url'] = url 

282 r['context'] = context 

283 if context is None: 

284 r['context'] = {} 

285 

286 

287def create_request_object(request_dict): 

288 """ 

289 This method takes a request dict and creates an AWSRequest object 

290 from it. 

291 

292 :type request_dict: dict 

293 :param request_dict: The request dict (created from the 

294 ``prepare_request_dict`` method). 

295 

296 :rtype: ``botocore.awsrequest.AWSRequest`` 

297 :return: An AWSRequest object based on the request_dict. 

298 

299 """ 

300 r = request_dict 

301 request_object = AWSRequest( 

302 method=r['method'], 

303 url=r['url'], 

304 data=r['body'], 

305 headers=r['headers'], 

306 auth_path=r.get('auth_path'), 

307 ) 

308 request_object.context = r['context'] 

309 return request_object 

310 

311 

312def _urljoin(endpoint_url, url_path, host_prefix): 

313 p = urlsplit(endpoint_url) 

314 # <part> - <index> 

315 # scheme - p[0] 

316 # netloc - p[1] 

317 # path - p[2] 

318 # query - p[3] 

319 # fragment - p[4] 

320 if not url_path or url_path == '/': 

321 # If there's no path component, ensure the URL ends with 

322 # a '/' for backwards compatibility. 

323 if not p[2]: 

324 new_path = '/' 

325 else: 

326 new_path = p[2] 

327 elif p[2].endswith('/') and url_path.startswith('/'): 

328 new_path = p[2][:-1] + url_path 

329 else: 

330 new_path = p[2] + url_path 

331 

332 new_netloc = p[1] 

333 if host_prefix is not None: 

334 new_netloc = host_prefix + new_netloc 

335 

336 reconstructed = urlunsplit((p[0], new_netloc, new_path, p[3], p[4])) 

337 return reconstructed 

338 

339 

340class AWSRequestPreparer: 

341 """ 

342 This class performs preparation on AWSRequest objects similar to that of 

343 the PreparedRequest class does in the requests library. However, the logic 

344 has been boiled down to meet the specific use cases in botocore. Of note 

345 there are the following differences: 

346 This class does not heavily prepare the URL. Requests performed many 

347 validations and corrections to ensure the URL is properly formatted. 

348 Botocore either performs these validations elsewhere or otherwise 

349 consistently provides well formatted URLs. 

350 

351 This class does not heavily prepare the body. Body preperation is 

352 simple and supports only the cases that we document: bytes and 

353 file-like objects to determine the content-length. This will also 

354 additionally prepare a body that is a dict to be url encoded params 

355 string as some signers rely on this. Finally, this class does not 

356 support multipart file uploads. 

357 

358 This class does not prepare the method, auth or cookies. 

359 """ 

360 

361 def prepare(self, original): 

362 method = original.method 

363 url = self._prepare_url(original) 

364 body = self._prepare_body(original) 

365 headers = self._prepare_headers(original, body) 

366 stream_output = original.stream_output 

367 

368 return AWSPreparedRequest(method, url, headers, body, stream_output) 

369 

370 def _prepare_url(self, original): 

371 url = original.url 

372 if original.params: 

373 url_parts = urlparse(url) 

374 delim = '&' if url_parts.query else '?' 

375 if isinstance(original.params, Mapping): 

376 params_to_encode = list(original.params.items()) 

377 else: 

378 params_to_encode = original.params 

379 params = urlencode(params_to_encode, doseq=True) 

380 url = delim.join((url, params)) 

381 return url 

382 

383 def _prepare_headers(self, original, prepared_body=None): 

384 headers = HeadersDict(original.headers.items()) 

385 

386 # If the transfer encoding or content length is already set, use that 

387 if 'Transfer-Encoding' in headers or 'Content-Length' in headers: 

388 return headers 

389 

390 # Ensure we set the content length when it is expected 

391 if original.method not in ('GET', 'HEAD', 'OPTIONS'): 

392 length = self._determine_content_length(prepared_body) 

393 if length is not None: 

394 headers['Content-Length'] = str(length) 

395 else: 

396 # Failed to determine content length, using chunked 

397 # NOTE: This shouldn't ever happen in practice 

398 body_type = type(prepared_body) 

399 logger.debug('Failed to determine length of %s', body_type) 

400 headers['Transfer-Encoding'] = 'chunked' 

401 

402 return headers 

403 

404 def _to_utf8(self, item): 

405 key, value = item 

406 if isinstance(key, str): 

407 key = key.encode('utf-8') 

408 if isinstance(value, str): 

409 value = value.encode('utf-8') 

410 return key, value 

411 

412 def _prepare_body(self, original): 

413 """Prepares the given HTTP body data.""" 

414 body = original.data 

415 if body == b'': 

416 body = None 

417 

418 if isinstance(body, dict): 

419 params = [self._to_utf8(item) for item in body.items()] 

420 body = urlencode(params, doseq=True) 

421 

422 return body 

423 

424 def _determine_content_length(self, body): 

425 return botocore.utils.determine_content_length(body) 

426 

427 

428class AWSRequest: 

429 """Represents the elements of an HTTP request. 

430 

431 This class was originally inspired by requests.models.Request, but has been 

432 boiled down to meet the specific use cases in botocore. That being said this 

433 class (even in requests) is effectively a named-tuple. 

434 """ 

435 

436 _REQUEST_PREPARER_CLS = AWSRequestPreparer 

437 

438 def __init__( 

439 self, 

440 method=None, 

441 url=None, 

442 headers=None, 

443 data=None, 

444 params=None, 

445 auth_path=None, 

446 stream_output=False, 

447 ): 

448 

449 self._request_preparer = self._REQUEST_PREPARER_CLS() 

450 

451 # Default empty dicts for dict params. 

452 params = {} if params is None else params 

453 

454 self.method = method 

455 self.url = url 

456 self.headers = HTTPHeaders() 

457 self.data = data 

458 self.params = params 

459 self.auth_path = auth_path 

460 self.stream_output = stream_output 

461 

462 if headers is not None: 

463 for key, value in headers.items(): 

464 self.headers[key] = value 

465 

466 # This is a dictionary to hold information that is used when 

467 # processing the request. What is inside of ``context`` is open-ended. 

468 # For example, it may have a timestamp key that is used for holding 

469 # what the timestamp is when signing the request. Note that none 

470 # of the information that is inside of ``context`` is directly 

471 # sent over the wire; the information is only used to assist in 

472 # creating what is sent over the wire. 

473 self.context = {} 

474 

475 def prepare(self): 

476 """Constructs a :class:`AWSPreparedRequest <AWSPreparedRequest>`.""" 

477 return self._request_preparer.prepare(self) 

478 

479 @property 

480 def body(self): 

481 body = self.prepare().body 

482 if isinstance(body, str): 

483 body = body.encode('utf-8') 

484 return body 

485 

486 

487class AWSPreparedRequest: 

488 """A data class representing a finalized request to be sent over the wire. 

489 

490 Requests at this stage should be treated as final, and the properties of 

491 the request should not be modified. 

492 

493 :ivar method: The HTTP Method 

494 :ivar url: The full url 

495 :ivar headers: The HTTP headers to send. 

496 :ivar body: The HTTP body. 

497 :ivar stream_output: If the response for this request should be streamed. 

498 """ 

499 

500 def __init__(self, method, url, headers, body, stream_output): 

501 self.method = method 

502 self.url = url 

503 self.headers = headers 

504 self.body = body 

505 self.stream_output = stream_output 

506 

507 def __repr__(self): 

508 fmt = ( 

509 '<AWSPreparedRequest stream_output=%s, method=%s, url=%s, ' 

510 'headers=%s>' 

511 ) 

512 return fmt % (self.stream_output, self.method, self.url, self.headers) 

513 

514 def reset_stream(self): 

515 """Resets the streaming body to it's initial position. 

516 

517 If the request contains a streaming body (a streamable file-like object) 

518 seek to the object's initial position to ensure the entire contents of 

519 the object is sent. This is a no-op for static bytes-like body types. 

520 """ 

521 # Trying to reset a stream when there is a no stream will 

522 # just immediately return. It's not an error, it will produce 

523 # the same result as if we had actually reset the stream (we'll send 

524 # the entire body contents again if we need to). 

525 # Same case if the body is a string/bytes/bytearray type. 

526 

527 non_seekable_types = (bytes, str, bytearray) 

528 if self.body is None or isinstance(self.body, non_seekable_types): 

529 return 

530 try: 

531 logger.debug("Rewinding stream: %s", self.body) 

532 self.body.seek(0) 

533 except Exception as e: 

534 logger.debug("Unable to rewind stream: %s", e) 

535 raise UnseekableStreamError(stream_object=self.body) 

536 

537 

538class AWSResponse: 

539 """A data class representing an HTTP response. 

540 

541 This class was originally inspired by requests.models.Response, but has 

542 been boiled down to meet the specific use cases in botocore. This has 

543 effectively been reduced to a named tuple. 

544 

545 :ivar url: The full url. 

546 :ivar status_code: The status code of the HTTP response. 

547 :ivar headers: The HTTP headers received. 

548 :ivar body: The HTTP response body. 

549 """ 

550 

551 def __init__(self, url, status_code, headers, raw): 

552 self.url = url 

553 self.status_code = status_code 

554 self.headers = HeadersDict(headers) 

555 self.raw = raw 

556 

557 self._content = None 

558 

559 @property 

560 def content(self): 

561 """Content of the response as bytes.""" 

562 

563 if self._content is None: 

564 # Read the contents. 

565 # NOTE: requests would attempt to call stream and fall back 

566 # to a custom generator that would call read in a loop, but 

567 # we don't rely on this behavior 

568 self._content = bytes().join(self.raw.stream()) or bytes() 

569 

570 return self._content 

571 

572 @property 

573 def text(self): 

574 """Content of the response as a proper text type. 

575 

576 Uses the encoding type provided in the reponse headers to decode the 

577 response content into a proper text type. If the encoding is not 

578 present in the headers, UTF-8 is used as a default. 

579 """ 

580 encoding = botocore.utils.get_encoding_from_headers(self.headers) 

581 if encoding: 

582 return self.content.decode(encoding) 

583 else: 

584 return self.content.decode('utf-8') 

585 

586 

587class _HeaderKey: 

588 def __init__(self, key): 

589 self._key = key 

590 self._lower = key.lower() 

591 

592 def __hash__(self): 

593 return hash(self._lower) 

594 

595 def __eq__(self, other): 

596 return isinstance(other, _HeaderKey) and self._lower == other._lower 

597 

598 def __str__(self): 

599 return self._key 

600 

601 def __repr__(self): 

602 return repr(self._key) 

603 

604 

605class HeadersDict(MutableMapping): 

606 """A case-insenseitive dictionary to represent HTTP headers.""" 

607 

608 def __init__(self, *args, **kwargs): 

609 self._dict = {} 

610 self.update(*args, **kwargs) 

611 

612 def __setitem__(self, key, value): 

613 self._dict[_HeaderKey(key)] = value 

614 

615 def __getitem__(self, key): 

616 return self._dict[_HeaderKey(key)] 

617 

618 def __delitem__(self, key): 

619 del self._dict[_HeaderKey(key)] 

620 

621 def __iter__(self): 

622 return (str(key) for key in self._dict) 

623 

624 def __len__(self): 

625 return len(self._dict) 

626 

627 def __repr__(self): 

628 return repr(self._dict) 

629 

630 def copy(self): 

631 return HeadersDict(self.items())