Team Ai
Datasetpublic

codekingpro/portable-devtools

sourceHugging Faceupdated 5mo agoView on Hugging Face
1likes14kdownloads
awsrequest.py636 linesDownload Raw Back to botocore
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"). You5# may not use this file except in compliance with the License. A copy of6# the License is located at7#8# http://aws.amazon.com/apache2.0/9#10# or in the "license" file accompanying this file. This file is11# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF12# ANY KIND, either express or implied. See the License for the specific13# language governing permissions and limitations under the License.14import functools15import logging16from collections.abc import Mapping17 18import urllib3.util19from urllib3.connection import HTTPConnection, VerifiedHTTPSConnection20from urllib3.connectionpool import HTTPConnectionPool, HTTPSConnectionPool21 22import botocore.utils23from botocore.compat import (24    HTTPHeaders,25    HTTPResponse,26    MutableMapping,27    urlencode,28    urlparse,29    urlsplit,30    urlunsplit,31)32from botocore.exceptions import UnseekableStreamError33 34logger = logging.getLogger(__name__)35 36 37class AWSHTTPResponse(HTTPResponse):38    # The *args, **kwargs is used because the args are slightly39    # 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_tuple47            self._status_tuple = None48            return status_tuple49        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 (though57    technically we subclass from urllib3, which subclasses58    httplib.HTTPConnection) and we only override this class to support Expect59    100-continue, which we need for S3.  As far as I can tell, this is60    general purpose enough to not be specific to S3, but I'm being61    tentative and keeping it in botocore because I've only tested62    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_class69        # This variable is set when we receive an early response from the70        # server. If this value is set to True, any calls to send() are noops.71        # This value is reset to false every time _send_request is called.72        # This is to workaround changes in urllib3 2.0 which uses separate73        # send() calls in request() instead of delegating to endheaders(),74        # which is where the body is sent in CPython's HTTPConnection.75        self._response_received = False76        self._expect_header_set = False77        self._send_called = False78 79    def close(self):80        super().close()81        # Reset all of our instance state we were tracking.82        self._response_received = False83        self._expect_header_set = False84        self._send_called = False85        self.response_class = self._original_response_cls86 87    def request(self, method, url, body=None, headers=None, *args, **kwargs):88        if headers is None:89            headers = {}90        self._response_received = False91        if headers.get('Expect', b'') == b'100-continue':92            self._expect_header_set = True93        else:94            self._expect_header_set = False95            self.response_class = self._original_response_cls96        rval = super().request(method, url, body, headers, *args, **kwargs)97        self._expect_header_set = False98        return rval99 100    def _convert_to_bytes(self, mixed_buffer):101        # Take a list of mixed str/bytes and convert it102        # 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 msg112 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 interaction119        # between delayed ack and the Nagle algorithm.120        if isinstance(message_body, bytes):121            msg += message_body122            message_body = None123        self.send(msg)124        if self._expect_header_set:125            # This is our custom behavior.  If the Expect header was126            # 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                return132            else:133                # From the RFC:134                # Because of the presence of older implementations, the135                # protocol allows ambiguous situations in which a client may136                # send "Expect: 100-continue" without receiving either a 417137                # (Expectation Failed) status or a 100 (Continue) status.138                # Therefore, when a client sends this header field to an origin139                # server (possibly via a proxy) from which it has never seen a140                # 100 (Continue) status, the client SHOULD NOT wait for an141                # 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), and148            # 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 return153        # the CLRF after the 100 continue response.  However,154        # some servers (I've specifically seen this for squid when155        # used as a straight HTTP proxy) will also inject a156        # Connection: keep-alive header.  To account for this157        # we'll read until we read '\r\n', and ignore any headers158        # that come immediately after the 100 continue response.159        current = None160        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 containing165        # 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 Expect182                #   request-header field with the "100-continue"183                #   expectation, an origin server MUST either respond with184                #   100 (Continue) status and continue to read from the185                #   input stream, or respond with a final status code.186                #187                # So if we don't get a 100 Continue response, then188                # whatever the server has sent back is the final response189                # 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_tuple201                )202                self.response_class = response_class203                self._response_received = True204        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            if not self._send_called:214                # urllib3 2.0 chunks and calls send potentially215                # thousands of times inside `request` unlike the216                # standard library. Only log this once for sanity.217                logger.debug(218                    "send() called, but response already received. "219                    "Not sending data."220                )221            self._send_called = True222            return223        return super().send(str)224 225    def _is_100_continue_status(self, maybe_status_line):226        parts = maybe_status_line.split(None, 2)227        # Check for HTTP/<version> 100 Continue\r\n228        return (229            len(parts) >= 3230            and parts[0].startswith(b'HTTP/')231            and parts[1] == b'100'232        )233 234 235class AWSHTTPConnection(AWSConnection, HTTPConnection):236    """An HTTPConnection that supports 100 Continue behavior."""237 238 239class AWSHTTPSConnection(AWSConnection, VerifiedHTTPSConnection):240    """An HTTPSConnection that supports 100 Continue behavior."""241 242 243class AWSHTTPConnectionPool(HTTPConnectionPool):244    ConnectionCls = AWSHTTPConnection245 246 247class AWSHTTPSConnectionPool(HTTPSConnectionPool):248    ConnectionCls = AWSHTTPSConnection249 250 251def prepare_request_dict(252    request_dict, endpoint_url, context=None, user_agent=None253):254    """255    This method prepares a request dict to be created into an256    AWSRequestObject. This prepares the request dict by adding the257    url and the user agent to the request dict.258 259    :type request_dict: dict260    :param request_dict:  The request dict (created from the261        ``serialize`` module).262 263    :type user_agent: string264    :param user_agent: The user agent to use for this request.265 266    :type endpoint_url: string267    :param endpoint_url: The full endpoint url, which contains at least268        the scheme, the hostname, and optionally any path components.269    """270    r = request_dict271    if user_agent is not None:272        headers = r['headers']273        headers['User-Agent'] = user_agent274    host_prefix = r.get('host_prefix')275    url = _urljoin(endpoint_url, r['url_path'], host_prefix)276    if r['query_string']:277        # NOTE: This is to avoid circular import with utils. This is being278        # done to avoid moving classes to different modules as to not cause279        # breaking chainges.280        percent_encode_sequence = botocore.utils.percent_encode_sequence281        encoded_query_string = percent_encode_sequence(r['query_string'])282        if '?' not in url:283            url += '?%s' % encoded_query_string284        else:285            url += '&%s' % encoded_query_string286    r['url'] = url287    r['context'] = context288    if context is None:289        r['context'] = {}290 291 292def create_request_object(request_dict):293    """294    This method takes a request dict and creates an AWSRequest object295    from it.296 297    :type request_dict: dict298    :param request_dict:  The request dict (created from the299        ``prepare_request_dict`` method).300 301    :rtype: ``botocore.awsrequest.AWSRequest``302    :return: An AWSRequest object based on the request_dict.303 304    """305    r = request_dict306    request_object = AWSRequest(307        method=r['method'],308        url=r['url'],309        data=r['body'],310        headers=r['headers'],311        auth_path=r.get('auth_path'),312    )313    request_object.context = r['context']314    return request_object315 316 317def _urljoin(endpoint_url, url_path, host_prefix):318    p = urlsplit(endpoint_url)319    # <part>   - <index>320    # scheme   - p[0]321    # netloc   - p[1]322    # path     - p[2]323    # query    - p[3]324    # fragment - p[4]325    if not url_path or url_path == '/':326        # If there's no path component, ensure the URL ends with327        # a '/' for backwards compatibility.328        if not p[2]:329            new_path = '/'330        else:331            new_path = p[2]332    elif p[2].endswith('/') and url_path.startswith('/'):333        new_path = p[2][:-1] + url_path334    else:335        new_path = p[2] + url_path336 337    new_netloc = p[1]338    if host_prefix is not None:339        new_netloc = host_prefix + new_netloc340 341    reconstructed = urlunsplit((p[0], new_netloc, new_path, p[3], p[4]))342    return reconstructed343 344 345class AWSRequestPreparer:346    """347    This class performs preparation on AWSRequest objects similar to that of348    the PreparedRequest class does in the requests library. However, the logic349    has been boiled down to meet the specific use cases in botocore. Of note350    there are the following differences:351        This class does not heavily prepare the URL. Requests performed many352        validations and corrections to ensure the URL is properly formatted.353        Botocore either performs these validations elsewhere or otherwise354        consistently provides well formatted URLs.355 356        This class does not heavily prepare the body. Body preperation is357        simple and supports only the cases that we document: bytes and358        file-like objects to determine the content-length. This will also359        additionally prepare a body that is a dict to be url encoded params360        string as some signers rely on this. Finally, this class does not361        support multipart file uploads.362 363        This class does not prepare the method, auth or cookies.364    """365 366    def prepare(self, original):367        method = original.method368        url = self._prepare_url(original)369        body = self._prepare_body(original)370        headers = self._prepare_headers(original, body)371        stream_output = original.stream_output372 373        return AWSPreparedRequest(method, url, headers, body, stream_output)374 375    def _prepare_url(self, original):376        url = original.url377        if original.params:378            url_parts = urlparse(url)379            delim = '&' if url_parts.query else '?'380            if isinstance(original.params, Mapping):381                params_to_encode = list(original.params.items())382            else:383                params_to_encode = original.params384            params = urlencode(params_to_encode, doseq=True)385            url = delim.join((url, params))386        return url387 388    def _prepare_headers(self, original, prepared_body=None):389        headers = HeadersDict(original.headers.items())390 391        # If the transfer encoding or content length is already set, use that392        if 'Transfer-Encoding' in headers or 'Content-Length' in headers:393            return headers394 395        # Ensure we set the content length when it is expected396        if original.method not in ('GET', 'HEAD', 'OPTIONS'):397            length = self._determine_content_length(prepared_body)398            if length is not None:399                headers['Content-Length'] = str(length)400            else:401                # Failed to determine content length, using chunked402                # NOTE: This shouldn't ever happen in practice403                body_type = type(prepared_body)404                logger.debug('Failed to determine length of %s', body_type)405                headers['Transfer-Encoding'] = 'chunked'406 407        return headers408 409    def _to_utf8(self, item):410        key, value = item411        if isinstance(key, str):412            key = key.encode('utf-8')413        if isinstance(value, str):414            value = value.encode('utf-8')415        return key, value416 417    def _prepare_body(self, original):418        """Prepares the given HTTP body data."""419        body = original.data420        if body == b'':421            body = None422 423        if isinstance(body, dict):424            params = [self._to_utf8(item) for item in body.items()]425            body = urlencode(params, doseq=True)426 427        return body428 429    def _determine_content_length(self, body):430        return botocore.utils.determine_content_length(body)431 432 433class AWSRequest:434    """Represents the elements of an HTTP request.435 436    This class was originally inspired by requests.models.Request, but has been437    boiled down to meet the specific use cases in botocore. That being said this438    class (even in requests) is effectively a named-tuple.439    """440 441    _REQUEST_PREPARER_CLS = AWSRequestPreparer442 443    def __init__(444        self,445        method=None,446        url=None,447        headers=None,448        data=None,449        params=None,450        auth_path=None,451        stream_output=False,452    ):453        self._request_preparer = self._REQUEST_PREPARER_CLS()454 455        # Default empty dicts for dict params.456        params = {} if params is None else params457 458        self.method = method459        self.url = url460        self.headers = HTTPHeaders()461        self.data = data462        self.params = params463        self.auth_path = auth_path464        self.stream_output = stream_output465 466        if headers is not None:467            for key, value in headers.items():468                self.headers[key] = value469 470        # This is a dictionary to hold information that is used when471        # processing the request. What is inside of ``context`` is open-ended.472        # For example, it may have a timestamp key that is used for holding473        # what the timestamp is when signing the request. Note that none474        # of the information that is inside of ``context`` is directly475        # sent over the wire; the information is only used to assist in476        # creating what is sent over the wire.477        self.context = {}478 479    def prepare(self):480        """Constructs a :class:`AWSPreparedRequest <AWSPreparedRequest>`."""481        return self._request_preparer.prepare(self)482 483    @property484    def body(self):485        body = self.prepare().body486        if isinstance(body, str):487            body = body.encode('utf-8')488        return body489 490 491class AWSPreparedRequest:492    """A data class representing a finalized request to be sent over the wire.493 494    Requests at this stage should be treated as final, and the properties of495    the request should not be modified.496 497    :ivar method: The HTTP Method498    :ivar url: The full url499    :ivar headers: The HTTP headers to send.500    :ivar body: The HTTP body.501    :ivar stream_output: If the response for this request should be streamed.502    """503 504    def __init__(self, method, url, headers, body, stream_output):505        self.method = method506        self.url = url507        self.headers = headers508        self.body = body509        self.stream_output = stream_output510 511    def __repr__(self):512        fmt = (513            '<AWSPreparedRequest stream_output=%s, method=%s, url=%s, '514            'headers=%s>'515        )516        return fmt % (self.stream_output, self.method, self.url, self.headers)517 518    def reset_stream(self):519        """Resets the streaming body to it's initial position.520 521        If the request contains a streaming body (a streamable file-like object)522        seek to the object's initial position to ensure the entire contents of523        the object is sent. This is a no-op for static bytes-like body types.524        """525        # Trying to reset a stream when there is a no stream will526        # just immediately return.  It's not an error, it will produce527        # the same result as if we had actually reset the stream (we'll send528        # the entire body contents again if we need to).529        # Same case if the body is a string/bytes/bytearray type.530 531        non_seekable_types = (bytes, str, bytearray)532        if self.body is None or isinstance(self.body, non_seekable_types):533            return534        try:535            logger.debug("Rewinding stream: %s", self.body)536            self.body.seek(0)537        except Exception as e:538            logger.debug("Unable to rewind stream: %s", e)539            raise UnseekableStreamError(stream_object=self.body)540 541 542class AWSResponse:543    """A data class representing an HTTP response.544 545    This class was originally inspired by requests.models.Response, but has546    been boiled down to meet the specific use cases in botocore. This has547    effectively been reduced to a named tuple.548 549    :ivar url: The full url.550    :ivar status_code: The status code of the HTTP response.551    :ivar headers: The HTTP headers received.552    :ivar body: The HTTP response body.553    """554 555    def __init__(self, url, status_code, headers, raw):556        self.url = url557        self.status_code = status_code558        self.headers = HeadersDict(headers)559        self.raw = raw560 561        self._content = None562 563    @property564    def content(self):565        """Content of the response as bytes."""566 567        if self._content is None:568            # Read the contents.569            # NOTE: requests would attempt to call stream and fall back570            # to a custom generator that would call read in a loop, but571            # we don't rely on this behavior572            self._content = b''.join(self.raw.stream()) or b''573 574        return self._content575 576    @property577    def text(self):578        """Content of the response as a proper text type.579 580        Uses the encoding type provided in the reponse headers to decode the581        response content into a proper text type. If the encoding is not582        present in the headers, UTF-8 is used as a default.583        """584        encoding = botocore.utils.get_encoding_from_headers(self.headers)585        if encoding:586            return self.content.decode(encoding)587        else:588            return self.content.decode('utf-8')589 590 591class _HeaderKey:592    def __init__(self, key):593        self._key = key594        self._lower = key.lower()595 596    def __hash__(self):597        return hash(self._lower)598 599    def __eq__(self, other):600        return isinstance(other, _HeaderKey) and self._lower == other._lower601 602    def __str__(self):603        return self._key604 605    def __repr__(self):606        return repr(self._key)607 608 609class HeadersDict(MutableMapping):610    """A case-insenseitive dictionary to represent HTTP headers."""611 612    def __init__(self, *args, **kwargs):613        self._dict = {}614        self.update(*args, **kwargs)615 616    def __setitem__(self, key, value):617        self._dict[_HeaderKey(key)] = value618 619    def __getitem__(self, key):620        return self._dict[_HeaderKey(key)]621 622    def __delitem__(self, key):623        del self._dict[_HeaderKey(key)]624 625    def __iter__(self):626        return (str(key) for key in self._dict)627 628    def __len__(self):629        return len(self._dict)630 631    def __repr__(self):632        return repr(self._dict)633 634    def copy(self):635        return HeadersDict(self.items())636 
codekingpro/portable-devtools · Team Ai