/brz/remove-bazaar

To get this branch, use:
bzr branch http://gegoxaren.bato24.eu/bzr/brz/remove-bazaar

« back to all changes in this revision

Viewing changes to breezy/transport/http/response.py

  • Committer: Jelmer Vernooij
  • Date: 2017-07-23 22:06:41 UTC
  • mfrom: (6738 trunk)
  • mto: This revision was merged to the branch mainline in revision 6739.
  • Revision ID: jelmer@jelmer.uk-20170723220641-69eczax9bmv8d6kk
Merge trunk, address review comments.

Show diffs side-by-side

added added

removed removed

Lines of Context:
21
21
responses.
22
22
"""
23
23
 
24
 
import cgi
25
 
from io import BytesIO
 
24
from __future__ import absolute_import
 
25
 
26
26
import os
27
 
import http.client as http_client
28
 
import email.utils as email_utils
 
27
import httplib
 
28
import rfc822
29
29
 
30
30
from ... import (
31
31
    errors,
32
32
    osutils,
33
33
    )
 
34
from ...sixish import (
 
35
    BytesIO,
 
36
    )
34
37
 
35
38
 
36
39
class ResponseFile(object):
39
42
    Only read() and seek() (forward) are supported.
40
43
 
41
44
    """
42
 
 
43
45
    def __init__(self, path, infile):
44
46
        """Constructor.
45
47
 
57
59
        Dummy implementation for consistency with the 'file' API.
58
60
        """
59
61
 
60
 
    def __enter__(self):
61
 
        return self
62
 
 
63
 
    def __exit__(self, exc_type, exc_val, exc_tb):
64
 
        return False  # propogate exceptions.
65
 
 
66
 
    def read(self, size=None):
 
62
    def read(self, size=-1):
67
63
        """Read size bytes from the current position in the file.
68
64
 
69
65
        :param size:  The number of bytes to read.  Leave unspecified or pass
70
66
            -1 to read to EOF.
71
67
        """
72
 
        data = self._file.read(size)
 
68
        data =  self._file.read(size)
73
69
        self._pos += len(data)
74
70
        return data
75
71
 
78
74
        self._pos += len(data)
79
75
        return data
80
76
 
81
 
    def readlines(self, size=None):
82
 
        data = self._file.readlines()
83
 
        self._pos += sum(map(len, data))
84
 
        return data
85
 
 
86
77
    def __iter__(self):
87
78
        while True:
88
79
            line = self.readline()
118
109
 
119
110
# multiple_range: boundary_header boundary (content_range_header data boundary)+
120
111
 
121
 
 
122
112
class RangeFile(ResponseFile):
123
113
    """File-like object that allow access to partial available data.
124
114
 
168
158
        The file should be at the beginning of the body, the first range
169
159
        definition is read and taken into account.
170
160
        """
171
 
        if not isinstance(boundary, bytes):
172
 
            raise TypeError(boundary)
173
161
        self._boundary = boundary
174
162
        # Decode the headers and setup the first range
175
163
        self.read_boundary()
177
165
 
178
166
    def read_boundary(self):
179
167
        """Read the boundary headers defining a new range"""
180
 
        boundary_line = b'\r\n'
181
 
        while boundary_line == b'\r\n':
 
168
        boundary_line = '\r\n'
 
169
        while boundary_line == '\r\n':
182
170
            # RFC2616 19.2 Additional CRLFs may precede the first boundary
183
171
            # string entity.
184
172
            # To be on the safe side we allow it before any boundary line
185
173
            boundary_line = self._file.readline()
186
174
 
187
 
        if boundary_line == b'':
 
175
        if boundary_line == '':
188
176
            # A timeout in the proxy server caused the response to end early.
189
177
            # See launchpad bug 198646.
190
178
            raise errors.HttpBoundaryMissing(
191
179
                self._path,
192
180
                self._boundary)
193
181
 
194
 
        if boundary_line != b'--' + self._boundary + b'\r\n':
195
 
            # email_utils.unquote() incorrectly unquotes strings enclosed in <>
 
182
        if boundary_line != '--' + self._boundary + '\r\n':
 
183
            # rfc822.unquote() incorrectly unquotes strings enclosed in <>
196
184
            # IIS 6 and 7 incorrectly wrap boundary strings in <>
197
185
            # together they make a beautiful bug, which we will be gracious
198
186
            # about here
199
187
            if (self._unquote_boundary(boundary_line) !=
200
 
                    b'--' + self._boundary + b'\r\n'):
 
188
                '--' + self._boundary + '\r\n'):
201
189
                raise errors.InvalidHttpResponse(
202
190
                    self._path,
203
191
                    "Expected a boundary (%s) line, got '%s'"
204
192
                    % (self._boundary, boundary_line))
205
193
 
206
194
    def _unquote_boundary(self, b):
207
 
        return b[:2] + email_utils.unquote(b[2:-2].decode('ascii')).encode('ascii') + b[-2:]
 
195
        return b[:2] + rfc822.unquote(b[2:-2]) + b[-2:]
208
196
 
209
197
    def read_range_definition(self):
210
198
        """Read a new range definition in a multi parts message.
212
200
        Parse the headers including the empty line following them so that we
213
201
        are ready to read the data itself.
214
202
        """
215
 
        self._headers = http_client.parse_headers(self._file)
 
203
        self._headers = httplib.HTTPMessage(self._file, seekable=0)
216
204
        # Extract the range definition
217
 
        content_range = self._headers.get('content-range', None)
 
205
        content_range = self._headers.getheader('content-range', None)
218
206
        if content_range is None:
219
207
            raise errors.InvalidHttpResponse(
220
208
                self._path,
285
273
            -1 to read to EOF.
286
274
        """
287
275
        if (self._size > 0
288
 
                and self._pos == self._start + self._size):
 
276
            and self._pos == self._start + self._size):
289
277
            if size == 0:
290
 
                return b''
 
278
                return ''
291
279
            else:
292
280
                self._seek_to_next_range()
293
281
        elif self._pos < self._start:
325
313
            final_pos = start_pos + offset
326
314
        elif whence == 2:
327
315
            if self._size > 0:
328
 
                final_pos = self._start + self._size + offset  # offset < 0
 
316
                final_pos = self._start + self._size + offset # offset < 0
329
317
            else:
330
318
                raise errors.InvalidRange(
331
319
                    self._path, self._pos,
351
339
                cur_limit = self._start + self._size
352
340
 
353
341
        size = final_pos - self._pos
354
 
        if size > 0:  # size can be < 0 if we crossed a range boundary
 
342
        if size > 0: # size can be < 0 if we crossed a range boundary
355
343
            # We don't need the data, just read it and throw it away
356
344
            self._checked_read(size)
357
345
 
359
347
        return self._pos
360
348
 
361
349
 
362
 
def handle_response(url, code, getheader, data):
 
350
def handle_response(url, code, msg, data):
363
351
    """Interpret the code & headers and wrap the provided data in a RangeFile.
364
352
 
365
353
    This is a factory method which returns an appropriate RangeFile based on
367
355
 
368
356
    :param url: The url being processed. Mostly for error reporting
369
357
    :param code: The integer HTTP response code
370
 
    :param getheader: Function for retrieving header
 
358
    :param msg: An HTTPMessage containing the headers for the response
371
359
    :param data: A file-like object that can be read() to get the
372
360
                 requested data
373
361
    :return: A file-like object that can seek()+read() the
378
366
        rfile = ResponseFile(url, data)
379
367
    elif code == 206:
380
368
        rfile = RangeFile(url, data)
381
 
        # When there is no content-type header we treat the response as
382
 
        # being of type 'application/octet-stream' as per RFC2616 section
383
 
        # 7.2.1.
384
 
        # Therefore it is obviously not multipart
385
 
        content_type = getheader('content-type', 'application/octet-stream')
386
 
        mimetype, options = cgi.parse_header(content_type)
387
 
        if mimetype == 'multipart/byteranges':
388
 
            rfile.set_boundary(options['boundary'].encode('ascii'))
 
369
        content_type = msg.getheader('content-type', None)
 
370
        if content_type is None:
 
371
            # When there is no content-type header we treat the response as
 
372
            # being of type 'application/octet-stream' as per RFC2616 section
 
373
            # 7.2.1.
 
374
            # Therefore it is obviously not multipart
 
375
            content_type = 'application/octet-stream'
 
376
            is_multipart = False
 
377
        else:
 
378
            is_multipart = (msg.getmaintype() == 'multipart'
 
379
                            and msg.getsubtype() == 'byteranges')
 
380
 
 
381
        if is_multipart:
 
382
            # Full fledged multipart response
 
383
            rfile.set_boundary(msg.getparam('boundary'))
389
384
        else:
390
385
            # A response to a range request, but not multipart
391
 
            content_range = getheader('content-range', None)
 
386
            content_range = msg.getheader('content-range', None)
392
387
            if content_range is None:
393
 
                raise errors.InvalidHttpResponse(
394
 
                    url, 'Missing the Content-Range header in a 206 range response')
 
388
                raise errors.InvalidHttpResponse(url,
 
389
                    'Missing the Content-Range header in a 206 range response')
395
390
            rfile.set_range_from_header(content_range)
396
391
    else:
397
 
        raise errors.UnexpectedHttpStatus(url, code)
 
392
        raise errors.InvalidHttpResponse(url,
 
393
                                         'Unknown response code %s' % code)
398
394
 
399
395
    return rfile
 
396