/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/__init__.py

  • Committer: Jelmer Vernooij
  • Date: 2019-05-29 03:22:34 UTC
  • mfrom: (7303 work)
  • mto: This revision was merged to the branch mainline in revision 7306.
  • Revision ID: jelmer@jelmer.uk-20190529032234-mt3fuws8gq03tapi
Merge trunk.

Show diffs side-by-side

added added

removed removed

Lines of Context:
21
21
 
22
22
from __future__ import absolute_import
23
23
 
 
24
DEBUG = 0
 
25
 
 
26
import base64
 
27
import errno
24
28
import os
25
29
import re
 
30
import socket
 
31
import ssl
26
32
import sys
 
33
import time
 
34
import urllib
27
35
import weakref
28
36
 
 
37
try:
 
38
    import http.client as http_client
 
39
except ImportError:
 
40
    import httplib as http_client
 
41
try:
 
42
    import urllib.request as urllib_request
 
43
except ImportError:  # python < 3
 
44
    import urllib2 as urllib_request
 
45
try:
 
46
    from urllib.parse import urljoin, splitport, splittype, splithost
 
47
except ImportError:
 
48
    from urlparse import urljoin
 
49
    from urllib import splitport, splittype, splithost
 
50
 
 
51
# TODO: handle_response should be integrated into the http/__init__.py
 
52
from .response import handle_response
 
53
 
 
54
# FIXME: Oversimplifying, two kind of exceptions should be
 
55
# raised, once a request is issued: URLError before we have been
 
56
# able to process the response, HTTPError after that. Process the
 
57
# response means we are able to leave the socket clean, so if we
 
58
# are not able to do that, we should close the connection. The
 
59
# actual code more or less do that, tests should be written to
 
60
# ensure that.
 
61
 
 
62
from ... import __version__ as breezy_version
29
63
from ... import (
 
64
    config,
30
65
    debug,
31
66
    errors,
 
67
    lazy_import,
 
68
    osutils,
 
69
    trace,
32
70
    transport,
33
71
    ui,
34
72
    urlutils,
35
 
    )
 
73
)
36
74
from ...bzr.smart import medium
 
75
from ...sixish import (
 
76
    PY3,
 
77
    reraise,
 
78
    text_type,
 
79
)
37
80
from ...trace import mutter
38
81
from ...transport import (
39
82
    ConnectedTransport,
40
83
    )
41
84
 
42
 
# TODO: handle_response should be integrated into the http/__init__.py
43
 
from .response import handle_response
44
 
from ._urllib2_wrappers import (
45
 
    Opener,
46
 
    Request,
47
 
    )
48
 
 
 
85
 
 
86
try:
 
87
    _ = (ssl.match_hostname, ssl.CertificateError)
 
88
except AttributeError:
 
89
    # Provide fallbacks for python < 2.7.9
 
90
    def match_hostname(cert, host):
 
91
        trace.warning(
 
92
            '%s cannot be verified, https certificates verification is only'
 
93
            ' available for python versions >= 2.7.9' % (host,))
 
94
    ssl.match_hostname = match_hostname
 
95
    ssl.CertificateError = ValueError
 
96
 
 
97
 
 
98
# Note for packagers: if there is no package providing certs for your platform,
 
99
# the curl project produces http://curl.haxx.se/ca/cacert.pem weekly.
 
100
_ssl_ca_certs_known_locations = [
 
101
    u'/etc/ssl/certs/ca-certificates.crt',  # Ubuntu/debian/gentoo
 
102
    u'/etc/pki/tls/certs/ca-bundle.crt',  # Fedora/CentOS/RH
 
103
    u'/etc/ssl/ca-bundle.pem',  # OpenSuse
 
104
    u'/etc/ssl/cert.pem',  # OpenSuse
 
105
    u"/usr/local/share/certs/ca-root-nss.crt",  # FreeBSD
 
106
    # XXX: Needs checking, can't trust the interweb ;) -- vila 2012-01-25
 
107
    u'/etc/openssl/certs/ca-certificates.crt',  # Solaris
 
108
]
 
109
 
 
110
 
 
111
def default_ca_certs():
 
112
    if sys.platform == 'win32':
 
113
        return os.path.join(os.path.dirname(sys.executable), u"cacert.pem")
 
114
    elif sys.platform == 'darwin':
 
115
        # FIXME: Needs some default value for osx, waiting for osx installers
 
116
        # guys feedback -- vila 2012-01-25
 
117
        pass
 
118
    else:
 
119
        # Try known locations for friendly OSes providing the root certificates
 
120
        # without making them hard to use for any https client.
 
121
        for path in _ssl_ca_certs_known_locations:
 
122
            if os.path.exists(path):
 
123
                # First found wins
 
124
                return path
 
125
    # A default path that makes sense and will be mentioned in the error
 
126
    # presented to the user, even if not correct for all platforms
 
127
    return _ssl_ca_certs_known_locations[0]
 
128
 
 
129
 
 
130
def ca_certs_from_store(path):
 
131
    if not os.path.exists(path):
 
132
        raise ValueError("ca certs path %s does not exist" % path)
 
133
    return path
 
134
 
 
135
 
 
136
def cert_reqs_from_store(unicode_str):
 
137
    import ssl
 
138
    try:
 
139
        return {"required": ssl.CERT_REQUIRED,
 
140
                "none": ssl.CERT_NONE}[unicode_str]
 
141
    except KeyError:
 
142
        raise ValueError("invalid value %s" % unicode_str)
 
143
 
 
144
 
 
145
def default_ca_reqs():
 
146
    if sys.platform in ('win32', 'darwin'):
 
147
        # FIXME: Once we get a native access to root certificates there, this
 
148
        # won't needed anymore. See http://pad.lv/920455 -- vila 2012-02-15
 
149
        return u'none'
 
150
    else:
 
151
        return u'required'
 
152
 
 
153
 
 
154
opt_ssl_ca_certs = config.Option('ssl.ca_certs',
 
155
                                 from_unicode=ca_certs_from_store,
 
156
                                 default=default_ca_certs,
 
157
                                 invalid='warning',
 
158
                                 help="""\
 
159
Path to certification authority certificates to trust.
 
160
 
 
161
This should be a valid path to a bundle containing all root Certificate
 
162
Authorities used to verify an https server certificate.
 
163
 
 
164
Use ssl.cert_reqs=none to disable certificate verification.
 
165
""")
 
166
 
 
167
opt_ssl_cert_reqs = config.Option('ssl.cert_reqs',
 
168
                                  default=default_ca_reqs,
 
169
                                  from_unicode=cert_reqs_from_store,
 
170
                                  invalid='error',
 
171
                                  help="""\
 
172
Whether to require a certificate from the remote side. (default:required)
 
173
 
 
174
Possible values:
 
175
 * none: Certificates ignored
 
176
 * required: Certificates required and validated
 
177
""")
 
178
 
 
179
checked_kerberos = False
 
180
kerberos = None
 
181
 
 
182
 
 
183
class addinfourl(urllib_request.addinfourl):
 
184
    '''Replacement addinfourl class compatible with python-2.7's xmlrpclib
 
185
 
 
186
    In python-2.7, xmlrpclib expects that the response object that it receives
 
187
    has a getheader method.  http_client.HTTPResponse provides this but
 
188
    urllib_request.addinfourl does not.  Add the necessary functions here, ported to
 
189
    use the internal data structures of addinfourl.
 
190
    '''
 
191
 
 
192
    def getheader(self, name, default=None):
 
193
        if self.headers is None:
 
194
            raise http_client.ResponseNotReady()
 
195
        return self.headers.getheader(name, default)
 
196
 
 
197
    def getheaders(self):
 
198
        if self.headers is None:
 
199
            raise http_client.ResponseNotReady()
 
200
        return list(self.headers.items())
 
201
 
 
202
 
 
203
class _ReportingFileSocket(object):
 
204
 
 
205
    def __init__(self, filesock, report_activity=None):
 
206
        self.filesock = filesock
 
207
        self._report_activity = report_activity
 
208
 
 
209
    def report_activity(self, size, direction):
 
210
        if self._report_activity:
 
211
            self._report_activity(size, direction)
 
212
 
 
213
    def read(self, size=1):
 
214
        s = self.filesock.read(size)
 
215
        self.report_activity(len(s), 'read')
 
216
        return s
 
217
 
 
218
    def readline(self, size=-1):
 
219
        s = self.filesock.readline(size)
 
220
        self.report_activity(len(s), 'read')
 
221
        return s
 
222
 
 
223
    def readinto(self, b):
 
224
        s = self.filesock.readinto(b)
 
225
        self.report_activity(s, 'read')
 
226
        return s
 
227
 
 
228
    def __getattr__(self, name):
 
229
        return getattr(self.filesock, name)
 
230
 
 
231
 
 
232
class _ReportingSocket(object):
 
233
 
 
234
    def __init__(self, sock, report_activity=None):
 
235
        self.sock = sock
 
236
        self._report_activity = report_activity
 
237
 
 
238
    def report_activity(self, size, direction):
 
239
        if self._report_activity:
 
240
            self._report_activity(size, direction)
 
241
 
 
242
    def sendall(self, s, *args):
 
243
        self.sock.sendall(s, *args)
 
244
        self.report_activity(len(s), 'write')
 
245
 
 
246
    def recv(self, *args):
 
247
        s = self.sock.recv(*args)
 
248
        self.report_activity(len(s), 'read')
 
249
        return s
 
250
 
 
251
    def makefile(self, mode='r', bufsize=-1):
 
252
        # http_client creates a fileobject that doesn't do buffering, which
 
253
        # makes fp.readline() very expensive because it only reads one byte
 
254
        # at a time.  So we wrap the socket in an object that forces
 
255
        # sock.makefile to make a buffered file.
 
256
        fsock = self.sock.makefile(mode, 65536)
 
257
        # And wrap that into a reporting kind of fileobject
 
258
        return _ReportingFileSocket(fsock, self._report_activity)
 
259
 
 
260
    def __getattr__(self, name):
 
261
        return getattr(self.sock, name)
 
262
 
 
263
 
 
264
# We define our own Response class to keep our http_client pipe clean
 
265
class Response(http_client.HTTPResponse):
 
266
    """Custom HTTPResponse, to avoid the need to decorate.
 
267
 
 
268
    http_client prefers to decorate the returned objects, rather
 
269
    than using a custom object.
 
270
    """
 
271
 
 
272
    # Some responses have bodies in which we have no interest
 
273
    _body_ignored_responses = [301, 302, 303, 307, 400, 401, 403, 404, 501]
 
274
 
 
275
    # in finish() below, we may have to discard several MB in the worst
 
276
    # case. To avoid buffering that much, we read and discard by chunks
 
277
    # instead. The underlying file is either a socket or a StringIO, so reading
 
278
    # 8k chunks should be fine.
 
279
    _discarded_buf_size = 8192
 
280
 
 
281
    if PY3:
 
282
        def __init__(self, sock, debuglevel=0, method=None, url=None):
 
283
            self.url = url
 
284
            super(Response, self).__init__(
 
285
                sock, debuglevel=debuglevel, method=method, url=url)
 
286
 
 
287
    def begin(self):
 
288
        """Begin to read the response from the server.
 
289
 
 
290
        http_client assumes that some responses get no content and do
 
291
        not even attempt to read the body in that case, leaving
 
292
        the body in the socket, blocking the next request. Let's
 
293
        try to workaround that.
 
294
        """
 
295
        http_client.HTTPResponse.begin(self)
 
296
        if self.status in self._body_ignored_responses:
 
297
            if self.debuglevel >= 2:
 
298
                print("For status: [%s], will ready body, length: %s" % (
 
299
                    self.status, self.length))
 
300
            if not (self.length is None or self.will_close):
 
301
                # In some cases, we just can't read the body not
 
302
                # even try or we may encounter a 104, 'Connection
 
303
                # reset by peer' error if there is indeed no body
 
304
                # and the server closed the connection just after
 
305
                # having issued the response headers (even if the
 
306
                # headers indicate a Content-Type...)
 
307
                body = self.read(self.length)
 
308
                if self.debuglevel >= 9:
 
309
                    # This one can be huge and is generally not interesting
 
310
                    print("Consumed body: [%s]" % body)
 
311
            self.close()
 
312
        elif self.status == 200:
 
313
            # Whatever the request is, it went ok, so we surely don't want to
 
314
            # close the connection. Some cases are not correctly detected by
 
315
            # http_client.HTTPConnection.getresponse (called by
 
316
            # http_client.HTTPResponse.begin). The CONNECT response for the https
 
317
            # through proxy case is one.  Note: the 'will_close' below refers
 
318
            # to the "true" socket between us and the server, whereas the
 
319
            # 'close()' above refers to the copy of that socket created by
 
320
            # http_client for the response itself. So, in the if above we close the
 
321
            # socket to indicate that we are done with the response whereas
 
322
            # below we keep the socket with the server opened.
 
323
            self.will_close = False
 
324
 
 
325
    def finish(self):
 
326
        """Finish reading the body.
 
327
 
 
328
        In some cases, the client may have left some bytes to read in the
 
329
        body. That will block the next request to succeed if we use a
 
330
        persistent connection. If we don't use a persistent connection, well,
 
331
        nothing will block the next request since a new connection will be
 
332
        issued anyway.
 
333
 
 
334
        :return: the number of bytes left on the socket (may be None)
 
335
        """
 
336
        pending = None
 
337
        if not self.isclosed():
 
338
            # Make sure nothing was left to be read on the socket
 
339
            pending = 0
 
340
            data = True
 
341
            while data and self.length:
 
342
                # read() will update self.length
 
343
                data = self.read(min(self.length, self._discarded_buf_size))
 
344
                pending += len(data)
 
345
            if pending:
 
346
                trace.mutter("%s bytes left on the HTTP socket", pending)
 
347
            self.close()
 
348
        return pending
 
349
 
 
350
 
 
351
# Not inheriting from 'object' because http_client.HTTPConnection doesn't.
 
352
class AbstractHTTPConnection:
 
353
    """A custom HTTP(S) Connection, which can reset itself on a bad response"""
 
354
 
 
355
    response_class = Response
 
356
 
 
357
    # When we detect a server responding with the whole file to range requests,
 
358
    # we want to warn. But not below a given thresold.
 
359
    _range_warning_thresold = 1024 * 1024
 
360
 
 
361
    def __init__(self, report_activity=None):
 
362
        self._response = None
 
363
        self._report_activity = report_activity
 
364
        self._ranges_received_whole_file = None
 
365
 
 
366
    def _mutter_connect(self):
 
367
        netloc = '%s:%s' % (self.host, self.port)
 
368
        if self.proxied_host is not None:
 
369
            netloc += '(proxy for %s)' % self.proxied_host
 
370
        trace.mutter('* About to connect() to %s' % netloc)
 
371
 
 
372
    def getresponse(self):
 
373
        """Capture the response to be able to cleanup"""
 
374
        self._response = http_client.HTTPConnection.getresponse(self)
 
375
        return self._response
 
376
 
 
377
    def cleanup_pipe(self):
 
378
        """Read the remaining bytes of the last response if any."""
 
379
        if self._response is not None:
 
380
            try:
 
381
                pending = self._response.finish()
 
382
                # Warn the user (once)
 
383
                if (self._ranges_received_whole_file is None
 
384
                        and self._response.status == 200
 
385
                        and pending
 
386
                        and pending > self._range_warning_thresold):
 
387
                    self._ranges_received_whole_file = True
 
388
                    trace.warning(
 
389
                        'Got a 200 response when asking for multiple ranges,'
 
390
                        ' does your server at %s:%s support range requests?',
 
391
                        self.host, self.port)
 
392
            except socket.error as e:
 
393
                # It's conceivable that the socket is in a bad state here
 
394
                # (including some test cases) and in this case, it doesn't need
 
395
                # cleaning anymore, so no need to fail, we just get rid of the
 
396
                # socket and let callers reconnect
 
397
                if (len(e.args) == 0
 
398
                        or e.args[0] not in (errno.ECONNRESET, errno.ECONNABORTED)):
 
399
                    raise
 
400
                self.close()
 
401
            self._response = None
 
402
        # Preserve our preciousss
 
403
        sock = self.sock
 
404
        self.sock = None
 
405
        # Let http_client.HTTPConnection do its housekeeping
 
406
        self.close()
 
407
        # Restore our preciousss
 
408
        self.sock = sock
 
409
 
 
410
    def _wrap_socket_for_reporting(self, sock):
 
411
        """Wrap the socket before anybody use it."""
 
412
        self.sock = _ReportingSocket(sock, self._report_activity)
 
413
 
 
414
 
 
415
class HTTPConnection(AbstractHTTPConnection, http_client.HTTPConnection):
 
416
 
 
417
    # XXX: Needs refactoring at the caller level.
 
418
    def __init__(self, host, port=None, proxied_host=None,
 
419
                 report_activity=None, ca_certs=None):
 
420
        AbstractHTTPConnection.__init__(self, report_activity=report_activity)
 
421
        if PY3:
 
422
            http_client.HTTPConnection.__init__(self, host, port)
 
423
        else:
 
424
            # Use strict=True since we don't support HTTP/0.9
 
425
            http_client.HTTPConnection.__init__(self, host, port, strict=True)
 
426
        self.proxied_host = proxied_host
 
427
        # ca_certs is ignored, it's only relevant for https
 
428
 
 
429
    def connect(self):
 
430
        if 'http' in debug.debug_flags:
 
431
            self._mutter_connect()
 
432
        http_client.HTTPConnection.connect(self)
 
433
        self._wrap_socket_for_reporting(self.sock)
 
434
 
 
435
 
 
436
class HTTPSConnection(AbstractHTTPConnection, http_client.HTTPSConnection):
 
437
 
 
438
    def __init__(self, host, port=None, key_file=None, cert_file=None,
 
439
                 proxied_host=None,
 
440
                 report_activity=None, ca_certs=None):
 
441
        AbstractHTTPConnection.__init__(self, report_activity=report_activity)
 
442
        if PY3:
 
443
            http_client.HTTPSConnection.__init__(
 
444
                self, host, port, key_file, cert_file)
 
445
        else:
 
446
            # Use strict=True since we don't support HTTP/0.9
 
447
            http_client.HTTPSConnection.__init__(self, host, port,
 
448
                                                 key_file, cert_file, strict=True)
 
449
        self.proxied_host = proxied_host
 
450
        self.ca_certs = ca_certs
 
451
 
 
452
    def connect(self):
 
453
        if 'http' in debug.debug_flags:
 
454
            self._mutter_connect()
 
455
        http_client.HTTPConnection.connect(self)
 
456
        self._wrap_socket_for_reporting(self.sock)
 
457
        if self.proxied_host is None:
 
458
            self.connect_to_origin()
 
459
 
 
460
    def connect_to_origin(self):
 
461
        # FIXME JRV 2011-12-18: Use location config here?
 
462
        config_stack = config.GlobalStack()
 
463
        cert_reqs = config_stack.get('ssl.cert_reqs')
 
464
        if self.proxied_host is not None:
 
465
            host = self.proxied_host.split(":", 1)[0]
 
466
        else:
 
467
            host = self.host
 
468
        if cert_reqs == ssl.CERT_NONE:
 
469
            ui.ui_factory.show_user_warning('not_checking_ssl_cert', host=host)
 
470
            ui.ui_factory.suppressed_warnings.add('not_checking_ssl_cert')
 
471
            ca_certs = None
 
472
        else:
 
473
            if self.ca_certs is None:
 
474
                ca_certs = config_stack.get('ssl.ca_certs')
 
475
            else:
 
476
                ca_certs = self.ca_certs
 
477
            if ca_certs is None:
 
478
                trace.warning(
 
479
                    "No valid trusted SSL CA certificates file set. See "
 
480
                    "'brz help ssl.ca_certs' for more information on setting "
 
481
                    "trusted CAs.")
 
482
        try:
 
483
            ssl_context = ssl.create_default_context(
 
484
                purpose=ssl.Purpose.SERVER_AUTH, cafile=ca_certs)
 
485
            ssl.check_hostname = True
 
486
            if self.cert_file:
 
487
                ssl_context.load_cert_chain(
 
488
                    keyfile=self.key_file, certfile=self.cert_file)
 
489
            ssl_context.verify_mode = cert_reqs
 
490
            ssl_sock = ssl_context.wrap_socket(
 
491
                self.sock, server_hostname=self.host)
 
492
        except ssl.SSLError:
 
493
            trace.note(
 
494
                "\n"
 
495
                "See `brz help ssl.ca_certs` for how to specify trusted CA"
 
496
                "certificates.\n"
 
497
                "Pass -Ossl.cert_reqs=none to disable certificate "
 
498
                "verification entirely.\n")
 
499
            raise
 
500
        # Wrap the ssl socket before anybody use it
 
501
        self._wrap_socket_for_reporting(ssl_sock)
 
502
 
 
503
 
 
504
class Request(urllib_request.Request):
 
505
    """A custom Request object.
 
506
 
 
507
    urllib_request determines the request method heuristically (based on
 
508
    the presence or absence of data). We set the method
 
509
    statically.
 
510
 
 
511
    The Request object tracks:
 
512
    - the connection the request will be made on.
 
513
    - the authentication parameters needed to preventively set
 
514
      the authentication header once a first authentication have
 
515
       been made.
 
516
    """
 
517
 
 
518
    def __init__(self, method, url, data=None, headers={},
 
519
                 origin_req_host=None, unverifiable=False,
 
520
                 connection=None, parent=None):
 
521
        urllib_request.Request.__init__(
 
522
            self, url, data, headers,
 
523
            origin_req_host, unverifiable)
 
524
        self.method = method
 
525
        self.connection = connection
 
526
        # To handle redirections
 
527
        self.parent = parent
 
528
        self.redirected_to = None
 
529
        # Unless told otherwise, redirections are not followed
 
530
        self.follow_redirections = False
 
531
        # auth and proxy_auth are dicts containing, at least
 
532
        # (scheme, host, port, realm, user, password, protocol, path).
 
533
        # The dict entries are mostly handled by the AuthHandler.
 
534
        # Some authentication schemes may add more entries.
 
535
        self.auth = {}
 
536
        self.proxy_auth = {}
 
537
        self.proxied_host = None
 
538
 
 
539
    def get_method(self):
 
540
        return self.method
 
541
 
 
542
    def set_proxy(self, proxy, type):
 
543
        """Set the proxy and remember the proxied host."""
 
544
        if PY3:
 
545
            host, port = splitport(self.host)
 
546
        else:
 
547
            host, port = splitport(self.get_host())
 
548
        if port is None:
 
549
            # We need to set the default port ourselves way before it gets set
 
550
            # in the HTTP[S]Connection object at build time.
 
551
            if self.type == 'https':
 
552
                conn_class = HTTPSConnection
 
553
            else:
 
554
                conn_class = HTTPConnection
 
555
            port = conn_class.default_port
 
556
        self.proxied_host = '%s:%s' % (host, port)
 
557
        urllib_request.Request.set_proxy(self, proxy, type)
 
558
        # When urllib_request makes a https request with our wrapper code and a proxy,
 
559
        # it sets Host to the https proxy, not the host we want to talk to.
 
560
        # I'm fairly sure this is our fault, but what is the cause is an open
 
561
        # question. -- Robert Collins May 8 2010.
 
562
        self.add_unredirected_header('Host', self.proxied_host)
 
563
 
 
564
 
 
565
class _ConnectRequest(Request):
 
566
 
 
567
    def __init__(self, request):
 
568
        """Constructor
 
569
 
 
570
        :param request: the first request sent to the proxied host, already
 
571
            processed by the opener (i.e. proxied_host is already set).
 
572
        """
 
573
        # We give a fake url and redefine selector or urllib_request will be
 
574
        # confused
 
575
        Request.__init__(self, 'CONNECT', request.get_full_url(),
 
576
                         connection=request.connection)
 
577
        if request.proxied_host is None:
 
578
            raise AssertionError()
 
579
        self.proxied_host = request.proxied_host
 
580
 
 
581
    @property
 
582
    def selector(self):
 
583
        return self.proxied_host
 
584
 
 
585
    def get_selector(self):
 
586
        return self.selector
 
587
 
 
588
    def set_proxy(self, proxy, type):
 
589
        """Set the proxy without remembering the proxied host.
 
590
 
 
591
        We already know the proxied host by definition, the CONNECT request
 
592
        occurs only when the connection goes through a proxy. The usual
 
593
        processing (masquerade the request so that the connection is done to
 
594
        the proxy while the request is targeted at another host) does not apply
 
595
        here. In fact, the connection is already established with proxy and we
 
596
        just want to enable the SSL tunneling.
 
597
        """
 
598
        urllib_request.Request.set_proxy(self, proxy, type)
 
599
 
 
600
 
 
601
class ConnectionHandler(urllib_request.BaseHandler):
 
602
    """Provides connection-sharing by pre-processing requests.
 
603
 
 
604
    urllib_request provides no way to access the HTTPConnection object
 
605
    internally used. But we need it in order to achieve
 
606
    connection sharing. So, we add it to the request just before
 
607
    it is processed, and then we override the do_open method for
 
608
    http[s] requests in AbstractHTTPHandler.
 
609
    """
 
610
 
 
611
    handler_order = 1000  # after all pre-processings
 
612
 
 
613
    def __init__(self, report_activity=None, ca_certs=None):
 
614
        self._report_activity = report_activity
 
615
        self.ca_certs = ca_certs
 
616
 
 
617
    def create_connection(self, request, http_connection_class):
 
618
        host = request.host
 
619
        if not host:
 
620
            # Just a bit of paranoia here, this should have been
 
621
            # handled in the higher levels
 
622
            raise urlutils.InvalidURL(request.get_full_url(), 'no host given.')
 
623
 
 
624
        # We create a connection (but it will not connect until the first
 
625
        # request is made)
 
626
        try:
 
627
            connection = http_connection_class(
 
628
                host, proxied_host=request.proxied_host,
 
629
                report_activity=self._report_activity,
 
630
                ca_certs=self.ca_certs)
 
631
        except http_client.InvalidURL as exception:
 
632
            # There is only one occurrence of InvalidURL in http_client
 
633
            raise urlutils.InvalidURL(request.get_full_url(),
 
634
                                      extra='nonnumeric port')
 
635
 
 
636
        return connection
 
637
 
 
638
    def capture_connection(self, request, http_connection_class):
 
639
        """Capture or inject the request connection.
 
640
 
 
641
        Two cases:
 
642
        - the request have no connection: create a new one,
 
643
 
 
644
        - the request have a connection: this one have been used
 
645
          already, let's capture it, so that we can give it to
 
646
          another transport to be reused. We don't do that
 
647
          ourselves: the Transport object get the connection from
 
648
          a first request and then propagate it, from request to
 
649
          request or to cloned transports.
 
650
        """
 
651
        connection = request.connection
 
652
        if connection is None:
 
653
            # Create a new one
 
654
            connection = self.create_connection(request, http_connection_class)
 
655
            request.connection = connection
 
656
 
 
657
        # All connections will pass here, propagate debug level
 
658
        connection.set_debuglevel(DEBUG)
 
659
        return request
 
660
 
 
661
    def http_request(self, request):
 
662
        return self.capture_connection(request, HTTPConnection)
 
663
 
 
664
    def https_request(self, request):
 
665
        return self.capture_connection(request, HTTPSConnection)
 
666
 
 
667
 
 
668
class AbstractHTTPHandler(urllib_request.AbstractHTTPHandler):
 
669
    """A custom handler for HTTP(S) requests.
 
670
 
 
671
    We overrive urllib_request.AbstractHTTPHandler to get a better
 
672
    control of the connection, the ability to implement new
 
673
    request types and return a response able to cope with
 
674
    persistent connections.
 
675
    """
 
676
 
 
677
    # We change our order to be before urllib_request HTTP[S]Handlers
 
678
    # and be chosen instead of them (the first http_open called
 
679
    # wins).
 
680
    handler_order = 400
 
681
 
 
682
    _default_headers = {'Pragma': 'no-cache',
 
683
                        'Cache-control': 'max-age=0',
 
684
                        'Connection': 'Keep-Alive',
 
685
                        'User-agent': 'Breezy/%s' % breezy_version,
 
686
                        'Accept': '*/*',
 
687
                        }
 
688
 
 
689
    def __init__(self):
 
690
        urllib_request.AbstractHTTPHandler.__init__(self, debuglevel=DEBUG)
 
691
 
 
692
    def http_request(self, request):
 
693
        """Common headers setting"""
 
694
 
 
695
        for name, value in self._default_headers.items():
 
696
            if name not in request.headers:
 
697
                request.headers[name] = value
 
698
        # FIXME: We may have to add the Content-Length header if
 
699
        # we have data to send.
 
700
        return request
 
701
 
 
702
    def retry_or_raise(self, http_class, request, first_try):
 
703
        """Retry the request (once) or raise the exception.
 
704
 
 
705
        urllib_request raises exception of application level kind, we
 
706
        just have to translate them.
 
707
 
 
708
        http_client can raise exceptions of transport level (badly
 
709
        formatted dialog, loss of connexion or socket level
 
710
        problems). In that case we should issue the request again
 
711
        (http_client will close and reopen a new connection if
 
712
        needed).
 
713
        """
 
714
        # When an exception occurs, we give back the original
 
715
        # Traceback or the bugs are hard to diagnose.
 
716
        exc_type, exc_val, exc_tb = sys.exc_info()
 
717
        if exc_type == socket.gaierror:
 
718
            # No need to retry, that will not help
 
719
            if PY3:
 
720
                origin_req_host = request.origin_req_host
 
721
            else:
 
722
                origin_req_host = request.get_origin_req_host()
 
723
            raise errors.ConnectionError("Couldn't resolve host '%s'"
 
724
                                         % origin_req_host,
 
725
                                         orig_error=exc_val)
 
726
        elif isinstance(exc_val, http_client.ImproperConnectionState):
 
727
            # The http_client pipeline is in incorrect state, it's a bug in our
 
728
            # implementation.
 
729
            reraise(exc_type, exc_val, exc_tb)
 
730
        else:
 
731
            if first_try:
 
732
                if self._debuglevel >= 2:
 
733
                    print('Received exception: [%r]' % exc_val)
 
734
                    print('  On connection: [%r]' % request.connection)
 
735
                    method = request.get_method()
 
736
                    url = request.get_full_url()
 
737
                    print('  Will retry, %s %r' % (method, url))
 
738
                request.connection.close()
 
739
                response = self.do_open(http_class, request, False)
 
740
            else:
 
741
                if self._debuglevel >= 2:
 
742
                    print('Received second exception: [%r]' % exc_val)
 
743
                    print('  On connection: [%r]' % request.connection)
 
744
                if exc_type in (http_client.BadStatusLine, http_client.UnknownProtocol):
 
745
                    # http_client.BadStatusLine and
 
746
                    # http_client.UnknownProtocol indicates that a
 
747
                    # bogus server was encountered or a bad
 
748
                    # connection (i.e. transient errors) is
 
749
                    # experimented, we have already retried once
 
750
                    # for that request so we raise the exception.
 
751
                    my_exception = errors.InvalidHttpResponse(
 
752
                        request.get_full_url(),
 
753
                        'Bad status line received',
 
754
                        orig_error=exc_val)
 
755
                elif (isinstance(exc_val, socket.error) and len(exc_val.args)
 
756
                      and exc_val.args[0] in (errno.ECONNRESET, 10053, 10054)):
 
757
                    # 10053 == WSAECONNABORTED
 
758
                    # 10054 == WSAECONNRESET
 
759
                    raise errors.ConnectionReset(
 
760
                        "Connection lost while sending request.")
 
761
                else:
 
762
                    # All other exception are considered connection related.
 
763
 
 
764
                    # socket errors generally occurs for reasons
 
765
                    # far outside our scope, so closing the
 
766
                    # connection and retrying is the best we can
 
767
                    # do.
 
768
                    if PY3:
 
769
                        selector = request.selector
 
770
                    else:
 
771
                        selector = request.get_selector()
 
772
                    my_exception = errors.ConnectionError(
 
773
                        msg='while sending %s %s:' % (request.get_method(),
 
774
                                                      selector),
 
775
                        orig_error=exc_val)
 
776
 
 
777
                if self._debuglevel >= 2:
 
778
                    print('On connection: [%r]' % request.connection)
 
779
                    method = request.get_method()
 
780
                    url = request.get_full_url()
 
781
                    print('  Failed again, %s %r' % (method, url))
 
782
                    print('  Will raise: [%r]' % my_exception)
 
783
                reraise(type(my_exception), my_exception, exc_tb)
 
784
        return response
 
785
 
 
786
    def do_open(self, http_class, request, first_try=True):
 
787
        """See urllib_request.AbstractHTTPHandler.do_open for the general idea.
 
788
 
 
789
        The request will be retried once if it fails.
 
790
        """
 
791
        connection = request.connection
 
792
        if connection is None:
 
793
            raise AssertionError(
 
794
                'Cannot process a request without a connection')
 
795
 
 
796
        # Get all the headers
 
797
        headers = {}
 
798
        headers.update(request.header_items())
 
799
        headers.update(request.unredirected_hdrs)
 
800
        # Some servers or proxies will choke on headers not properly
 
801
        # cased. http_client/urllib/urllib_request all use capitalize to get canonical
 
802
        # header names, but only python2.5 urllib_request use title() to fix them just
 
803
        # before sending the request. And not all versions of python 2.5 do
 
804
        # that. Since we replace urllib_request.AbstractHTTPHandler.do_open we do it
 
805
        # ourself below.
 
806
        headers = {name.title(): val for name, val in headers.items()}
 
807
 
 
808
        try:
 
809
            method = request.get_method()
 
810
            if PY3:
 
811
                url = request.selector
 
812
            else:
 
813
                url = request.get_selector()
 
814
            if sys.version_info[:2] >= (3, 6):
 
815
                connection._send_request(method, url,
 
816
                                         # FIXME: implements 100-continue
 
817
                                         # None, # We don't send the body yet
 
818
                                         request.data,
 
819
                                         headers, encode_chunked=False)
 
820
            else:
 
821
                connection._send_request(method, url,
 
822
                                         # FIXME: implements 100-continue
 
823
                                         # None, # We don't send the body yet
 
824
                                         request.data,
 
825
                                         headers)
 
826
            if 'http' in debug.debug_flags:
 
827
                trace.mutter('> %s %s' % (method, url))
 
828
                hdrs = []
 
829
                for k, v in headers.items():
 
830
                    # People are often told to paste -Dhttp output to help
 
831
                    # debug. Don't compromise credentials.
 
832
                    if k in ('Authorization', 'Proxy-Authorization'):
 
833
                        v = '<masked>'
 
834
                    hdrs.append('%s: %s' % (k, v))
 
835
                trace.mutter('> ' + '\n> '.join(hdrs) + '\n')
 
836
            if self._debuglevel >= 1:
 
837
                print('Request sent: [%r] from (%s)'
 
838
                      % (request, request.connection.sock.getsockname()))
 
839
            response = connection.getresponse()
 
840
            convert_to_addinfourl = True
 
841
        except (ssl.SSLError, ssl.CertificateError):
 
842
            # Something is wrong with either the certificate or the hostname,
 
843
            # re-trying won't help
 
844
            raise
 
845
        except (socket.gaierror, http_client.BadStatusLine, http_client.UnknownProtocol,
 
846
                socket.error, http_client.HTTPException):
 
847
            response = self.retry_or_raise(http_class, request, first_try)
 
848
            convert_to_addinfourl = False
 
849
 
 
850
        if PY3:
 
851
            response.msg = response.reason
 
852
            return response
 
853
 
 
854
# FIXME: HTTPConnection does not fully support 100-continue (the
 
855
# server responses are just ignored)
 
856
 
 
857
#        if code == 100:
 
858
#            mutter('Will send the body')
 
859
#            # We can send the body now
 
860
#            body = request.data
 
861
#            if body is None:
 
862
#                raise URLError("No data given")
 
863
#            connection.send(body)
 
864
#            response = connection.getresponse()
 
865
 
 
866
        if self._debuglevel >= 2:
 
867
            print('Receives response: %r' % response)
 
868
            print('  For: %r(%r)' % (request.get_method(),
 
869
                                     request.get_full_url()))
 
870
 
 
871
        if convert_to_addinfourl:
 
872
            # Shamelessly copied from urllib_request
 
873
            req = request
 
874
            r = response
 
875
            r.recv = r.read
 
876
            fp = socket._fileobject(r, bufsize=65536)
 
877
            resp = addinfourl(fp, r.msg, req.get_full_url())
 
878
            resp.code = r.status
 
879
            resp.msg = r.reason
 
880
            resp.version = r.version
 
881
            if self._debuglevel >= 2:
 
882
                print('Create addinfourl: %r' % resp)
 
883
                print('  For: %r(%r)' % (request.get_method(),
 
884
                                         request.get_full_url()))
 
885
            if 'http' in debug.debug_flags:
 
886
                version = 'HTTP/%d.%d'
 
887
                try:
 
888
                    version = version % (resp.version / 10,
 
889
                                         resp.version % 10)
 
890
                except:
 
891
                    version = 'HTTP/%r' % resp.version
 
892
                trace.mutter('< %s %s %s' % (version, resp.code,
 
893
                                             resp.msg))
 
894
                # Use the raw header lines instead of treating resp.info() as a
 
895
                # dict since we may miss duplicated headers otherwise.
 
896
                hdrs = [h.rstrip('\r\n') for h in resp.info().headers]
 
897
                trace.mutter('< ' + '\n< '.join(hdrs) + '\n')
 
898
        else:
 
899
            resp = response
 
900
        return resp
 
901
 
 
902
 
 
903
class HTTPHandler(AbstractHTTPHandler):
 
904
    """A custom handler that just thunks into HTTPConnection"""
 
905
 
 
906
    def http_open(self, request):
 
907
        return self.do_open(HTTPConnection, request)
 
908
 
 
909
 
 
910
class HTTPSHandler(AbstractHTTPHandler):
 
911
    """A custom handler that just thunks into HTTPSConnection"""
 
912
 
 
913
    https_request = AbstractHTTPHandler.http_request
 
914
 
 
915
    def https_open(self, request):
 
916
        connection = request.connection
 
917
        if connection.sock is None and \
 
918
                connection.proxied_host is not None and \
 
919
                request.get_method() != 'CONNECT':  # Don't loop
 
920
            # FIXME: We need a gazillion connection tests here, but we still
 
921
            # miss a https server :-( :
 
922
            # - with and without proxy
 
923
            # - with and without certificate
 
924
            # - with self-signed certificate
 
925
            # - with and without authentication
 
926
            # - with good and bad credentials (especially the proxy auth around
 
927
            #   CONNECT)
 
928
            # - with basic and digest schemes
 
929
            # - reconnection on errors
 
930
            # - connection persistence behaviour (including reconnection)
 
931
 
 
932
            # We are about to connect for the first time via a proxy, we must
 
933
            # issue a CONNECT request first to establish the encrypted link
 
934
            connect = _ConnectRequest(request)
 
935
            response = self.parent.open(connect)
 
936
            if response.code != 200:
 
937
                raise errors.ConnectionError("Can't connect to %s via proxy %s" % (
 
938
                    connect.proxied_host, self.host))
 
939
            # Housekeeping
 
940
            connection.cleanup_pipe()
 
941
            # Establish the connection encryption
 
942
            connection.connect_to_origin()
 
943
            # Propagate the connection to the original request
 
944
            request.connection = connection
 
945
        return self.do_open(HTTPSConnection, request)
 
946
 
 
947
 
 
948
class HTTPRedirectHandler(urllib_request.HTTPRedirectHandler):
 
949
    """Handles redirect requests.
 
950
 
 
951
    We have to implement our own scheme because we use a specific
 
952
    Request object and because we want to implement a specific
 
953
    policy.
 
954
    """
 
955
    _debuglevel = DEBUG
 
956
    # RFC2616 says that only read requests should be redirected
 
957
    # without interacting with the user. But Breezy uses some
 
958
    # shortcuts to optimize against roundtrips which can leads to
 
959
    # write requests being issued before read requests of
 
960
    # containing dirs can be redirected. So we redirect write
 
961
    # requests in the same way which seems to respect the spirit
 
962
    # of the RFC if not its letter.
 
963
 
 
964
    def redirect_request(self, req, fp, code, msg, headers, newurl):
 
965
        """See urllib_request.HTTPRedirectHandler.redirect_request"""
 
966
        # We would have preferred to update the request instead
 
967
        # of creating a new one, but the urllib_request.Request object
 
968
        # has a too complicated creation process to provide a
 
969
        # simple enough equivalent update process. Instead, when
 
970
        # redirecting, we only update the following request in
 
971
        # the redirect chain with a reference to the parent
 
972
        # request .
 
973
 
 
974
        # Some codes make no sense in our context and are treated
 
975
        # as errors:
 
976
 
 
977
        # 300: Multiple choices for different representations of
 
978
        #      the URI. Using that mechanisn with Breezy will violate the
 
979
        #      protocol neutrality of Transport.
 
980
 
 
981
        # 304: Not modified (SHOULD only occurs with conditional
 
982
        #      GETs which are not used by our implementation)
 
983
 
 
984
        # 305: Use proxy. I can't imagine this one occurring in
 
985
        #      our context-- vila/20060909
 
986
 
 
987
        # 306: Unused (if the RFC says so...)
 
988
 
 
989
        # If the code is 302 and the request is HEAD, some may
 
990
        # think that it is a sufficent hint that the file exists
 
991
        # and that we MAY avoid following the redirections. But
 
992
        # if we want to be sure, we MUST follow them.
 
993
 
 
994
        if PY3:
 
995
            origin_req_host = req.origin_req_host
 
996
        else:
 
997
            origin_req_host = req.get_origin_req_host()
 
998
 
 
999
        if code in (301, 302, 303, 307):
 
1000
            return Request(req.get_method(), newurl,
 
1001
                           headers=req.headers,
 
1002
                           origin_req_host=origin_req_host,
 
1003
                           unverifiable=True,
 
1004
                           # TODO: It will be nice to be able to
 
1005
                           # detect virtual hosts sharing the same
 
1006
                           # IP address, that will allow us to
 
1007
                           # share the same connection...
 
1008
                           connection=None,
 
1009
                           parent=req,
 
1010
                           )
 
1011
        else:
 
1012
            raise urllib_request.HTTPError(
 
1013
                req.get_full_url(), code, msg, headers, fp)
 
1014
 
 
1015
    def http_error_302(self, req, fp, code, msg, headers):
 
1016
        """Requests the redirected to URI.
 
1017
 
 
1018
        Copied from urllib_request to be able to clean the pipe of the associated
 
1019
        connection, *before* issuing the redirected request but *after* having
 
1020
        eventually raised an error.
 
1021
        """
 
1022
        # Some servers (incorrectly) return multiple Location headers
 
1023
        # (so probably same goes for URI).  Use first header.
 
1024
 
 
1025
        # TODO: Once we get rid of addinfourl objects, the
 
1026
        # following will need to be updated to use correct case
 
1027
        # for headers.
 
1028
        if 'location' in headers:
 
1029
            newurl = headers.get('location')
 
1030
        elif 'uri' in headers:
 
1031
            newurl = headers.get('uri')
 
1032
        else:
 
1033
            return
 
1034
        if self._debuglevel >= 1:
 
1035
            print('Redirected to: %s (followed: %r)' % (newurl,
 
1036
                                                        req.follow_redirections))
 
1037
        if req.follow_redirections is False:
 
1038
            req.redirected_to = newurl
 
1039
            return fp
 
1040
 
 
1041
        newurl = urljoin(req.get_full_url(), newurl)
 
1042
 
 
1043
        # This call succeeds or raise an error. urllib_request returns
 
1044
        # if redirect_request returns None, but our
 
1045
        # redirect_request never returns None.
 
1046
        redirected_req = self.redirect_request(req, fp, code, msg, headers,
 
1047
                                               newurl)
 
1048
 
 
1049
        # loop detection
 
1050
        # .redirect_dict has a key url if url was previously visited.
 
1051
        if hasattr(req, 'redirect_dict'):
 
1052
            visited = redirected_req.redirect_dict = req.redirect_dict
 
1053
            if (visited.get(newurl, 0) >= self.max_repeats or
 
1054
                    len(visited) >= self.max_redirections):
 
1055
                raise urllib_request.HTTPError(req.get_full_url(), code,
 
1056
                                               self.inf_msg + msg, headers, fp)
 
1057
        else:
 
1058
            visited = redirected_req.redirect_dict = req.redirect_dict = {}
 
1059
        visited[newurl] = visited.get(newurl, 0) + 1
 
1060
 
 
1061
        # We can close the fp now that we are sure that we won't
 
1062
        # use it with HTTPError.
 
1063
        fp.close()
 
1064
        # We have all we need already in the response
 
1065
        req.connection.cleanup_pipe()
 
1066
 
 
1067
        return self.parent.open(redirected_req)
 
1068
 
 
1069
    http_error_301 = http_error_303 = http_error_307 = http_error_302
 
1070
 
 
1071
 
 
1072
class ProxyHandler(urllib_request.ProxyHandler):
 
1073
    """Handles proxy setting.
 
1074
 
 
1075
    Copied and modified from urllib_request to be able to modify the request during
 
1076
    the request pre-processing instead of modifying it at _open time. As we
 
1077
    capture (or create) the connection object during request processing, _open
 
1078
    time was too late.
 
1079
 
 
1080
    The main task is to modify the request so that the connection is done to
 
1081
    the proxy while the request still refers to the destination host.
 
1082
 
 
1083
    Note: the proxy handling *may* modify the protocol used; the request may be
 
1084
    against an https server proxied through an http proxy. So, https_request
 
1085
    will be called, but later it's really http_open that will be called. This
 
1086
    explains why we don't have to call self.parent.open as the urllib_request did.
 
1087
    """
 
1088
 
 
1089
    # Proxies must be in front
 
1090
    handler_order = 100
 
1091
    _debuglevel = DEBUG
 
1092
 
 
1093
    def __init__(self, proxies=None):
 
1094
        urllib_request.ProxyHandler.__init__(self, proxies)
 
1095
        # First, let's get rid of urllib_request implementation
 
1096
        for type, proxy in self.proxies.items():
 
1097
            if self._debuglevel >= 3:
 
1098
                print('Will unbind %s_open for %r' % (type, proxy))
 
1099
            delattr(self, '%s_open' % type)
 
1100
 
 
1101
        def bind_scheme_request(proxy, scheme):
 
1102
            if proxy is None:
 
1103
                return
 
1104
            scheme_request = scheme + '_request'
 
1105
            if self._debuglevel >= 3:
 
1106
                print('Will bind %s for %r' % (scheme_request, proxy))
 
1107
            setattr(self, scheme_request,
 
1108
                    lambda request: self.set_proxy(request, scheme))
 
1109
        # We are interested only by the http[s] proxies
 
1110
        http_proxy = self.get_proxy_env_var('http')
 
1111
        bind_scheme_request(http_proxy, 'http')
 
1112
        https_proxy = self.get_proxy_env_var('https')
 
1113
        bind_scheme_request(https_proxy, 'https')
 
1114
 
 
1115
    def get_proxy_env_var(self, name, default_to='all'):
 
1116
        """Get a proxy env var.
 
1117
 
 
1118
        Note that we indirectly rely on
 
1119
        urllib.getproxies_environment taking into account the
 
1120
        uppercased values for proxy variables.
 
1121
        """
 
1122
        try:
 
1123
            return self.proxies[name.lower()]
 
1124
        except KeyError:
 
1125
            if default_to is not None:
 
1126
                # Try to get the alternate environment variable
 
1127
                try:
 
1128
                    return self.proxies[default_to]
 
1129
                except KeyError:
 
1130
                    pass
 
1131
        return None
 
1132
 
 
1133
    def proxy_bypass(self, host):
 
1134
        """Check if host should be proxied or not.
 
1135
 
 
1136
        :returns: True to skip the proxy, False otherwise.
 
1137
        """
 
1138
        no_proxy = self.get_proxy_env_var('no', default_to=None)
 
1139
        bypass = self.evaluate_proxy_bypass(host, no_proxy)
 
1140
        if bypass is None:
 
1141
            # Nevertheless, there are platform-specific ways to
 
1142
            # ignore proxies...
 
1143
            return urllib.proxy_bypass(host)
 
1144
        else:
 
1145
            return bypass
 
1146
 
 
1147
    def evaluate_proxy_bypass(self, host, no_proxy):
 
1148
        """Check the host against a comma-separated no_proxy list as a string.
 
1149
 
 
1150
        :param host: ``host:port`` being requested
 
1151
 
 
1152
        :param no_proxy: comma-separated list of hosts to access directly.
 
1153
 
 
1154
        :returns: True to skip the proxy, False not to, or None to
 
1155
            leave it to urllib.
 
1156
        """
 
1157
        if no_proxy is None:
 
1158
            # All hosts are proxied
 
1159
            return False
 
1160
        hhost, hport = splitport(host)
 
1161
        # Does host match any of the domains mentioned in
 
1162
        # no_proxy ? The rules about what is authorized in no_proxy
 
1163
        # are fuzzy (to say the least). We try to allow most
 
1164
        # commonly seen values.
 
1165
        for domain in no_proxy.split(','):
 
1166
            domain = domain.strip()
 
1167
            if domain == '':
 
1168
                continue
 
1169
            dhost, dport = splitport(domain)
 
1170
            if hport == dport or dport is None:
 
1171
                # Protect glob chars
 
1172
                dhost = dhost.replace(".", r"\.")
 
1173
                dhost = dhost.replace("*", r".*")
 
1174
                dhost = dhost.replace("?", r".")
 
1175
                if re.match(dhost, hhost, re.IGNORECASE):
 
1176
                    return True
 
1177
        # Nothing explicitly avoid the host
 
1178
        return None
 
1179
 
 
1180
    def set_proxy(self, request, type):
 
1181
        if PY3:
 
1182
            host = request.host
 
1183
        else:
 
1184
            host = request.get_host()
 
1185
        if self.proxy_bypass(host):
 
1186
            return request
 
1187
 
 
1188
        proxy = self.get_proxy_env_var(type)
 
1189
        if self._debuglevel >= 3:
 
1190
            print('set_proxy %s_request for %r' % (type, proxy))
 
1191
        # FIXME: python 2.5 urlparse provides a better _parse_proxy which can
 
1192
        # grok user:password@host:port as well as
 
1193
        # http://user:password@host:port
 
1194
 
 
1195
        parsed_url = transport.ConnectedTransport._split_url(proxy)
 
1196
        if not parsed_url.host:
 
1197
            raise urlutils.InvalidURL(proxy, 'No host component')
 
1198
 
 
1199
        if request.proxy_auth == {}:
 
1200
            # No proxy auth parameter are available, we are handling the first
 
1201
            # proxied request, intialize.  scheme (the authentication scheme)
 
1202
            # and realm will be set by the AuthHandler
 
1203
            request.proxy_auth = {
 
1204
                'host': parsed_url.host,
 
1205
                'port': parsed_url.port,
 
1206
                'user': parsed_url.user,
 
1207
                'password': parsed_url.password,
 
1208
                'protocol': parsed_url.scheme,
 
1209
                # We ignore path since we connect to a proxy
 
1210
                'path': None}
 
1211
        if parsed_url.port is None:
 
1212
            phost = parsed_url.host
 
1213
        else:
 
1214
            phost = parsed_url.host + ':%d' % parsed_url.port
 
1215
        request.set_proxy(phost, type)
 
1216
        if self._debuglevel >= 3:
 
1217
            print('set_proxy: proxy set to %s://%s' % (type, phost))
 
1218
        return request
 
1219
 
 
1220
 
 
1221
class AbstractAuthHandler(urllib_request.BaseHandler):
 
1222
    """A custom abstract authentication handler for all http authentications.
 
1223
 
 
1224
    Provides the meat to handle authentication errors and
 
1225
    preventively set authentication headers after the first
 
1226
    successful authentication.
 
1227
 
 
1228
    This can be used for http and proxy, as well as for basic, negotiate and
 
1229
    digest authentications.
 
1230
 
 
1231
    This provides an unified interface for all authentication handlers
 
1232
    (urllib_request provides far too many with different policies).
 
1233
 
 
1234
    The interaction between this handler and the urllib_request
 
1235
    framework is not obvious, it works as follow:
 
1236
 
 
1237
    opener.open(request) is called:
 
1238
 
 
1239
    - that may trigger http_request which will add an authentication header
 
1240
      (self.build_header) if enough info is available.
 
1241
 
 
1242
    - the request is sent to the server,
 
1243
 
 
1244
    - if an authentication error is received self.auth_required is called,
 
1245
      we acquire the authentication info in the error headers and call
 
1246
      self.auth_match to check that we are able to try the
 
1247
      authentication and complete the authentication parameters,
 
1248
 
 
1249
    - we call parent.open(request), that may trigger http_request
 
1250
      and will add a header (self.build_header), but here we have
 
1251
      all the required info (keep in mind that the request and
 
1252
      authentication used in the recursive calls are really (and must be)
 
1253
      the *same* objects).
 
1254
 
 
1255
    - if the call returns a response, the authentication have been
 
1256
      successful and the request authentication parameters have been updated.
 
1257
    """
 
1258
 
 
1259
    scheme = None
 
1260
    """The scheme as it appears in the server header (lower cased)"""
 
1261
 
 
1262
    _max_retry = 3
 
1263
    """We don't want to retry authenticating endlessly"""
 
1264
 
 
1265
    requires_username = True
 
1266
    """Whether the auth mechanism requires a username."""
 
1267
 
 
1268
    # The following attributes should be defined by daughter
 
1269
    # classes:
 
1270
    # - auth_required_header:  the header received from the server
 
1271
    # - auth_header: the header sent in the request
 
1272
 
 
1273
    def __init__(self):
 
1274
        # We want to know when we enter into an try/fail cycle of
 
1275
        # authentications so we initialize to None to indicate that we aren't
 
1276
        # in such a cycle by default.
 
1277
        self._retry_count = None
 
1278
 
 
1279
    def _parse_auth_header(self, server_header):
 
1280
        """Parse the authentication header.
 
1281
 
 
1282
        :param server_header: The value of the header sent by the server
 
1283
            describing the authenticaion request.
 
1284
 
 
1285
        :return: A tuple (scheme, remainder) scheme being the first word in the
 
1286
            given header (lower cased), remainder may be None.
 
1287
        """
 
1288
        try:
 
1289
            scheme, remainder = server_header.split(None, 1)
 
1290
        except ValueError:
 
1291
            scheme = server_header
 
1292
            remainder = None
 
1293
        return (scheme.lower(), remainder)
 
1294
 
 
1295
    def update_auth(self, auth, key, value):
 
1296
        """Update a value in auth marking the auth as modified if needed"""
 
1297
        old_value = auth.get(key, None)
 
1298
        if old_value != value:
 
1299
            auth[key] = value
 
1300
            auth['modified'] = True
 
1301
 
 
1302
    def auth_required(self, request, headers):
 
1303
        """Retry the request if the auth scheme is ours.
 
1304
 
 
1305
        :param request: The request needing authentication.
 
1306
        :param headers: The headers for the authentication error response.
 
1307
        :return: None or the response for the authenticated request.
 
1308
        """
 
1309
        # Don't try  to authenticate endlessly
 
1310
        if self._retry_count is None:
 
1311
            # The retry being recusrsive calls, None identify the first retry
 
1312
            self._retry_count = 1
 
1313
        else:
 
1314
            self._retry_count += 1
 
1315
            if self._retry_count > self._max_retry:
 
1316
                # Let's be ready for next round
 
1317
                self._retry_count = None
 
1318
                return None
 
1319
        if PY3:
 
1320
            server_headers = headers.get_all(self.auth_required_header)
 
1321
        else:
 
1322
            server_headers = headers.getheaders(self.auth_required_header)
 
1323
        if not server_headers:
 
1324
            # The http error MUST have the associated
 
1325
            # header. This must never happen in production code.
 
1326
            raise KeyError('%s not found' % self.auth_required_header)
 
1327
 
 
1328
        auth = self.get_auth(request)
 
1329
        auth['modified'] = False
 
1330
        # Put some common info in auth if the caller didn't
 
1331
        if auth.get('path', None) is None:
 
1332
            parsed_url = urlutils.URL.from_string(request.get_full_url())
 
1333
            self.update_auth(auth, 'protocol', parsed_url.scheme)
 
1334
            self.update_auth(auth, 'host', parsed_url.host)
 
1335
            self.update_auth(auth, 'port', parsed_url.port)
 
1336
            self.update_auth(auth, 'path', parsed_url.path)
 
1337
        # FIXME: the auth handler should be selected at a single place instead
 
1338
        # of letting all handlers try to match all headers, but the current
 
1339
        # design doesn't allow a simple implementation.
 
1340
        for server_header in server_headers:
 
1341
            # Several schemes can be proposed by the server, try to match each
 
1342
            # one in turn
 
1343
            matching_handler = self.auth_match(server_header, auth)
 
1344
            if matching_handler:
 
1345
                # auth_match may have modified auth (by adding the
 
1346
                # password or changing the realm, for example)
 
1347
                if (request.get_header(self.auth_header, None) is not None
 
1348
                        and not auth['modified']):
 
1349
                    # We already tried that, give up
 
1350
                    return None
 
1351
 
 
1352
                # Only the most secure scheme proposed by the server should be
 
1353
                # used, since the handlers use 'handler_order' to describe that
 
1354
                # property, the first handler tried takes precedence, the
 
1355
                # others should not attempt to authenticate if the best one
 
1356
                # failed.
 
1357
                best_scheme = auth.get('best_scheme', None)
 
1358
                if best_scheme is None:
 
1359
                    # At that point, if current handler should doesn't succeed
 
1360
                    # the credentials are wrong (or incomplete), but we know
 
1361
                    # that the associated scheme should be used.
 
1362
                    best_scheme = auth['best_scheme'] = self.scheme
 
1363
                if best_scheme != self.scheme:
 
1364
                    continue
 
1365
 
 
1366
                if self.requires_username and auth.get('user', None) is None:
 
1367
                    # Without a known user, we can't authenticate
 
1368
                    return None
 
1369
 
 
1370
                # Housekeeping
 
1371
                request.connection.cleanup_pipe()
 
1372
                # Retry the request with an authentication header added
 
1373
                response = self.parent.open(request)
 
1374
                if response:
 
1375
                    self.auth_successful(request, response)
 
1376
                return response
 
1377
        # We are not qualified to handle the authentication.
 
1378
        # Note: the authentication error handling will try all
 
1379
        # available handlers. If one of them authenticates
 
1380
        # successfully, a response will be returned. If none of
 
1381
        # them succeeds, None will be returned and the error
 
1382
        # handler will raise the 401 'Unauthorized' or the 407
 
1383
        # 'Proxy Authentication Required' error.
 
1384
        return None
 
1385
 
 
1386
    def add_auth_header(self, request, header):
 
1387
        """Add the authentication header to the request"""
 
1388
        request.add_unredirected_header(self.auth_header, header)
 
1389
 
 
1390
    def auth_match(self, header, auth):
 
1391
        """Check that we are able to handle that authentication scheme.
 
1392
 
 
1393
        The request authentication parameters may need to be
 
1394
        updated with info from the server. Some of these
 
1395
        parameters, when combined, are considered to be the
 
1396
        authentication key, if one of them change the
 
1397
        authentication result may change. 'user' and 'password'
 
1398
        are exampls, but some auth schemes may have others
 
1399
        (digest's nonce is an example, digest's nonce_count is a
 
1400
        *counter-example*). Such parameters must be updated by
 
1401
        using the update_auth() method.
 
1402
 
 
1403
        :param header: The authentication header sent by the server.
 
1404
        :param auth: The auth parameters already known. They may be
 
1405
             updated.
 
1406
        :returns: True if we can try to handle the authentication.
 
1407
        """
 
1408
        raise NotImplementedError(self.auth_match)
 
1409
 
 
1410
    def build_auth_header(self, auth, request):
 
1411
        """Build the value of the header used to authenticate.
 
1412
 
 
1413
        :param auth: The auth parameters needed to build the header.
 
1414
        :param request: The request needing authentication.
 
1415
 
 
1416
        :return: None or header.
 
1417
        """
 
1418
        raise NotImplementedError(self.build_auth_header)
 
1419
 
 
1420
    def auth_successful(self, request, response):
 
1421
        """The authentification was successful for the request.
 
1422
 
 
1423
        Additional infos may be available in the response.
 
1424
 
 
1425
        :param request: The succesfully authenticated request.
 
1426
        :param response: The server response (may contain auth info).
 
1427
        """
 
1428
        # It may happen that we need to reconnect later, let's be ready
 
1429
        self._retry_count = None
 
1430
 
 
1431
    def get_user_password(self, auth):
 
1432
        """Ask user for a password if none is already available.
 
1433
 
 
1434
        :param auth: authentication info gathered so far (from the initial url
 
1435
            and then during dialog with the server).
 
1436
        """
 
1437
        auth_conf = config.AuthenticationConfig()
 
1438
        user = auth.get('user', None)
 
1439
        password = auth.get('password', None)
 
1440
        realm = auth['realm']
 
1441
        port = auth.get('port', None)
 
1442
 
 
1443
        if user is None:
 
1444
            user = auth_conf.get_user(auth['protocol'], auth['host'],
 
1445
                                      port=port, path=auth['path'],
 
1446
                                      realm=realm, ask=True,
 
1447
                                      prompt=self.build_username_prompt(auth))
 
1448
        if user is not None and password is None:
 
1449
            password = auth_conf.get_password(
 
1450
                auth['protocol'], auth['host'], user,
 
1451
                port=port,
 
1452
                path=auth['path'], realm=realm,
 
1453
                prompt=self.build_password_prompt(auth))
 
1454
 
 
1455
        return user, password
 
1456
 
 
1457
    def _build_password_prompt(self, auth):
 
1458
        """Build a prompt taking the protocol used into account.
 
1459
 
 
1460
        The AuthHandler is used by http and https, we want that information in
 
1461
        the prompt, so we build the prompt from the authentication dict which
 
1462
        contains all the needed parts.
 
1463
 
 
1464
        Also, http and proxy AuthHandlers present different prompts to the
 
1465
        user. The daughter classes should implements a public
 
1466
        build_password_prompt using this method.
 
1467
        """
 
1468
        prompt = u'%s' % auth['protocol'].upper() + u' %(user)s@%(host)s'
 
1469
        realm = auth['realm']
 
1470
        if realm is not None:
 
1471
            prompt += u", Realm: '%s'" % realm
 
1472
        prompt += u' password'
 
1473
        return prompt
 
1474
 
 
1475
    def _build_username_prompt(self, auth):
 
1476
        """Build a prompt taking the protocol used into account.
 
1477
 
 
1478
        The AuthHandler is used by http and https, we want that information in
 
1479
        the prompt, so we build the prompt from the authentication dict which
 
1480
        contains all the needed parts.
 
1481
 
 
1482
        Also, http and proxy AuthHandlers present different prompts to the
 
1483
        user. The daughter classes should implements a public
 
1484
        build_username_prompt using this method.
 
1485
        """
 
1486
        prompt = u'%s' % auth['protocol'].upper() + u' %(host)s'
 
1487
        realm = auth['realm']
 
1488
        if realm is not None:
 
1489
            prompt += u", Realm: '%s'" % realm
 
1490
        prompt += u' username'
 
1491
        return prompt
 
1492
 
 
1493
    def http_request(self, request):
 
1494
        """Insert an authentication header if information is available"""
 
1495
        auth = self.get_auth(request)
 
1496
        if self.auth_params_reusable(auth):
 
1497
            self.add_auth_header(
 
1498
                request, self.build_auth_header(auth, request))
 
1499
        return request
 
1500
 
 
1501
    https_request = http_request  # FIXME: Need test
 
1502
 
 
1503
 
 
1504
class NegotiateAuthHandler(AbstractAuthHandler):
 
1505
    """A authentication handler that handles WWW-Authenticate: Negotiate.
 
1506
 
 
1507
    At the moment this handler supports just Kerberos. In the future,
 
1508
    NTLM support may also be added.
 
1509
    """
 
1510
 
 
1511
    scheme = 'negotiate'
 
1512
    handler_order = 480
 
1513
    requires_username = False
 
1514
 
 
1515
    def auth_match(self, header, auth):
 
1516
        scheme, raw_auth = self._parse_auth_header(header)
 
1517
        if scheme != self.scheme:
 
1518
            return False
 
1519
        self.update_auth(auth, 'scheme', scheme)
 
1520
        resp = self._auth_match_kerberos(auth)
 
1521
        if resp is None:
 
1522
            return False
 
1523
        # Optionally should try to authenticate using NTLM here
 
1524
        self.update_auth(auth, 'negotiate_response', resp)
 
1525
        return True
 
1526
 
 
1527
    def _auth_match_kerberos(self, auth):
 
1528
        """Try to create a GSSAPI response for authenticating against a host."""
 
1529
        global kerberos, checked_kerberos
 
1530
        if kerberos is None and not checked_kerberos:
 
1531
            try:
 
1532
                import kerberos
 
1533
            except ImportError:
 
1534
                kerberos = None
 
1535
            checked_kerberos = True
 
1536
        if kerberos is None:
 
1537
            return None
 
1538
        ret, vc = kerberos.authGSSClientInit("HTTP@%(host)s" % auth)
 
1539
        if ret < 1:
 
1540
            trace.warning('Unable to create GSSAPI context for %s: %d',
 
1541
                          auth['host'], ret)
 
1542
            return None
 
1543
        ret = kerberos.authGSSClientStep(vc, "")
 
1544
        if ret < 0:
 
1545
            trace.mutter('authGSSClientStep failed: %d', ret)
 
1546
            return None
 
1547
        return kerberos.authGSSClientResponse(vc)
 
1548
 
 
1549
    def build_auth_header(self, auth, request):
 
1550
        return "Negotiate %s" % auth['negotiate_response']
 
1551
 
 
1552
    def auth_params_reusable(self, auth):
 
1553
        # If the auth scheme is known, it means a previous
 
1554
        # authentication was successful, all information is
 
1555
        # available, no further checks are needed.
 
1556
        return (auth.get('scheme', None) == 'negotiate' and
 
1557
                auth.get('negotiate_response', None) is not None)
 
1558
 
 
1559
 
 
1560
class BasicAuthHandler(AbstractAuthHandler):
 
1561
    """A custom basic authentication handler."""
 
1562
 
 
1563
    scheme = 'basic'
 
1564
    handler_order = 500
 
1565
    auth_regexp = re.compile('realm="([^"]*)"', re.I)
 
1566
 
 
1567
    def build_auth_header(self, auth, request):
 
1568
        raw = '%s:%s' % (auth['user'], auth['password'])
 
1569
        auth_header = 'Basic ' + \
 
1570
            base64.b64encode(raw.encode('utf-8')).decode('ascii')
 
1571
        return auth_header
 
1572
 
 
1573
    def extract_realm(self, header_value):
 
1574
        match = self.auth_regexp.search(header_value)
 
1575
        realm = None
 
1576
        if match:
 
1577
            realm = match.group(1)
 
1578
        return match, realm
 
1579
 
 
1580
    def auth_match(self, header, auth):
 
1581
        scheme, raw_auth = self._parse_auth_header(header)
 
1582
        if scheme != self.scheme:
 
1583
            return False
 
1584
 
 
1585
        match, realm = self.extract_realm(raw_auth)
 
1586
        if match:
 
1587
            # Put useful info into auth
 
1588
            self.update_auth(auth, 'scheme', scheme)
 
1589
            self.update_auth(auth, 'realm', realm)
 
1590
            if (auth.get('user', None) is None
 
1591
                    or auth.get('password', None) is None):
 
1592
                user, password = self.get_user_password(auth)
 
1593
                self.update_auth(auth, 'user', user)
 
1594
                self.update_auth(auth, 'password', password)
 
1595
        return match is not None
 
1596
 
 
1597
    def auth_params_reusable(self, auth):
 
1598
        # If the auth scheme is known, it means a previous
 
1599
        # authentication was successful, all information is
 
1600
        # available, no further checks are needed.
 
1601
        return auth.get('scheme', None) == 'basic'
 
1602
 
 
1603
 
 
1604
def get_digest_algorithm_impls(algorithm):
 
1605
    H = None
 
1606
    KD = None
 
1607
    if algorithm == 'MD5':
 
1608
        def H(x): return osutils.md5(x).hexdigest()
 
1609
    elif algorithm == 'SHA':
 
1610
        H = osutils.sha_string
 
1611
    if H is not None:
 
1612
        def KD(secret, data): return H(
 
1613
            ("%s:%s" % (secret, data)).encode('utf-8'))
 
1614
    return H, KD
 
1615
 
 
1616
 
 
1617
def get_new_cnonce(nonce, nonce_count):
 
1618
    raw = '%s:%d:%s:%s' % (nonce, nonce_count, time.ctime(),
 
1619
                           osutils.rand_chars(8))
 
1620
    return osutils.sha_string(raw.encode('utf-8'))[:16]
 
1621
 
 
1622
 
 
1623
class DigestAuthHandler(AbstractAuthHandler):
 
1624
    """A custom digest authentication handler."""
 
1625
 
 
1626
    scheme = 'digest'
 
1627
    # Before basic as digest is a bit more secure and should be preferred
 
1628
    handler_order = 490
 
1629
 
 
1630
    def auth_params_reusable(self, auth):
 
1631
        # If the auth scheme is known, it means a previous
 
1632
        # authentication was successful, all information is
 
1633
        # available, no further checks are needed.
 
1634
        return auth.get('scheme', None) == 'digest'
 
1635
 
 
1636
    def auth_match(self, header, auth):
 
1637
        scheme, raw_auth = self._parse_auth_header(header)
 
1638
        if scheme != self.scheme:
 
1639
            return False
 
1640
 
 
1641
        # Put the requested authentication info into a dict
 
1642
        req_auth = urllib_request.parse_keqv_list(
 
1643
            urllib_request.parse_http_list(raw_auth))
 
1644
 
 
1645
        # Check that we can handle that authentication
 
1646
        qop = req_auth.get('qop', None)
 
1647
        if qop != 'auth':  # No auth-int so far
 
1648
            return False
 
1649
 
 
1650
        H, KD = get_digest_algorithm_impls(req_auth.get('algorithm', 'MD5'))
 
1651
        if H is None:
 
1652
            return False
 
1653
 
 
1654
        realm = req_auth.get('realm', None)
 
1655
        # Put useful info into auth
 
1656
        self.update_auth(auth, 'scheme', scheme)
 
1657
        self.update_auth(auth, 'realm', realm)
 
1658
        if auth.get('user', None) is None or auth.get('password', None) is None:
 
1659
            user, password = self.get_user_password(auth)
 
1660
            self.update_auth(auth, 'user', user)
 
1661
            self.update_auth(auth, 'password', password)
 
1662
 
 
1663
        try:
 
1664
            if req_auth.get('algorithm', None) is not None:
 
1665
                self.update_auth(auth, 'algorithm', req_auth.get('algorithm'))
 
1666
            nonce = req_auth['nonce']
 
1667
            if auth.get('nonce', None) != nonce:
 
1668
                # A new nonce, never used
 
1669
                self.update_auth(auth, 'nonce_count', 0)
 
1670
            self.update_auth(auth, 'nonce', nonce)
 
1671
            self.update_auth(auth, 'qop', qop)
 
1672
            auth['opaque'] = req_auth.get('opaque', None)
 
1673
        except KeyError:
 
1674
            # Some required field is not there
 
1675
            return False
 
1676
 
 
1677
        return True
 
1678
 
 
1679
    def build_auth_header(self, auth, request):
 
1680
        if PY3:
 
1681
            selector = request.selector
 
1682
        else:
 
1683
            selector = request.get_selector()
 
1684
        url_scheme, url_selector = splittype(selector)
 
1685
        sel_host, uri = splithost(url_selector)
 
1686
 
 
1687
        A1 = ('%s:%s:%s' %
 
1688
              (auth['user'], auth['realm'], auth['password'])).encode('utf-8')
 
1689
        A2 = ('%s:%s' % (request.get_method(), uri)).encode('utf-8')
 
1690
 
 
1691
        nonce = auth['nonce']
 
1692
        qop = auth['qop']
 
1693
 
 
1694
        nonce_count = auth['nonce_count'] + 1
 
1695
        ncvalue = '%08x' % nonce_count
 
1696
        cnonce = get_new_cnonce(nonce, nonce_count)
 
1697
 
 
1698
        H, KD = get_digest_algorithm_impls(auth.get('algorithm', 'MD5'))
 
1699
        nonce_data = '%s:%s:%s:%s:%s' % (nonce, ncvalue, cnonce, qop, H(A2))
 
1700
        request_digest = KD(H(A1), nonce_data)
 
1701
 
 
1702
        header = 'Digest '
 
1703
        header += 'username="%s", realm="%s", nonce="%s"' % (auth['user'],
 
1704
                                                             auth['realm'],
 
1705
                                                             nonce)
 
1706
        header += ', uri="%s"' % uri
 
1707
        header += ', cnonce="%s", nc=%s' % (cnonce, ncvalue)
 
1708
        header += ', qop="%s"' % qop
 
1709
        header += ', response="%s"' % request_digest
 
1710
        # Append the optional fields
 
1711
        opaque = auth.get('opaque', None)
 
1712
        if opaque:
 
1713
            header += ', opaque="%s"' % opaque
 
1714
        if auth.get('algorithm', None):
 
1715
            header += ', algorithm="%s"' % auth.get('algorithm')
 
1716
 
 
1717
        # We have used the nonce once more, update the count
 
1718
        auth['nonce_count'] = nonce_count
 
1719
 
 
1720
        return header
 
1721
 
 
1722
 
 
1723
class HTTPAuthHandler(AbstractAuthHandler):
 
1724
    """Custom http authentication handler.
 
1725
 
 
1726
    Send the authentication preventively to avoid the roundtrip
 
1727
    associated with the 401 error and keep the revelant info in
 
1728
    the auth request attribute.
 
1729
    """
 
1730
 
 
1731
    auth_required_header = 'www-authenticate'
 
1732
    auth_header = 'Authorization'
 
1733
 
 
1734
    def get_auth(self, request):
 
1735
        """Get the auth params from the request"""
 
1736
        return request.auth
 
1737
 
 
1738
    def set_auth(self, request, auth):
 
1739
        """Set the auth params for the request"""
 
1740
        request.auth = auth
 
1741
 
 
1742
    def build_password_prompt(self, auth):
 
1743
        return self._build_password_prompt(auth)
 
1744
 
 
1745
    def build_username_prompt(self, auth):
 
1746
        return self._build_username_prompt(auth)
 
1747
 
 
1748
    def http_error_401(self, req, fp, code, msg, headers):
 
1749
        return self.auth_required(req, headers)
 
1750
 
 
1751
 
 
1752
class ProxyAuthHandler(AbstractAuthHandler):
 
1753
    """Custom proxy authentication handler.
 
1754
 
 
1755
    Send the authentication preventively to avoid the roundtrip
 
1756
    associated with the 407 error and keep the revelant info in
 
1757
    the proxy_auth request attribute..
 
1758
    """
 
1759
 
 
1760
    auth_required_header = 'proxy-authenticate'
 
1761
    # FIXME: the correct capitalization is Proxy-Authorization,
 
1762
    # but python-2.4 urllib_request.Request insist on using capitalize()
 
1763
    # instead of title().
 
1764
    auth_header = 'Proxy-authorization'
 
1765
 
 
1766
    def get_auth(self, request):
 
1767
        """Get the auth params from the request"""
 
1768
        return request.proxy_auth
 
1769
 
 
1770
    def set_auth(self, request, auth):
 
1771
        """Set the auth params for the request"""
 
1772
        request.proxy_auth = auth
 
1773
 
 
1774
    def build_password_prompt(self, auth):
 
1775
        prompt = self._build_password_prompt(auth)
 
1776
        prompt = u'Proxy ' + prompt
 
1777
        return prompt
 
1778
 
 
1779
    def build_username_prompt(self, auth):
 
1780
        prompt = self._build_username_prompt(auth)
 
1781
        prompt = u'Proxy ' + prompt
 
1782
        return prompt
 
1783
 
 
1784
    def http_error_407(self, req, fp, code, msg, headers):
 
1785
        return self.auth_required(req, headers)
 
1786
 
 
1787
 
 
1788
class HTTPBasicAuthHandler(BasicAuthHandler, HTTPAuthHandler):
 
1789
    """Custom http basic authentication handler"""
 
1790
 
 
1791
 
 
1792
class ProxyBasicAuthHandler(BasicAuthHandler, ProxyAuthHandler):
 
1793
    """Custom proxy basic authentication handler"""
 
1794
 
 
1795
 
 
1796
class HTTPDigestAuthHandler(DigestAuthHandler, HTTPAuthHandler):
 
1797
    """Custom http basic authentication handler"""
 
1798
 
 
1799
 
 
1800
class ProxyDigestAuthHandler(DigestAuthHandler, ProxyAuthHandler):
 
1801
    """Custom proxy basic authentication handler"""
 
1802
 
 
1803
 
 
1804
class HTTPNegotiateAuthHandler(NegotiateAuthHandler, HTTPAuthHandler):
 
1805
    """Custom http negotiate authentication handler"""
 
1806
 
 
1807
 
 
1808
class ProxyNegotiateAuthHandler(NegotiateAuthHandler, ProxyAuthHandler):
 
1809
    """Custom proxy negotiate authentication handler"""
 
1810
 
 
1811
 
 
1812
class HTTPErrorProcessor(urllib_request.HTTPErrorProcessor):
 
1813
    """Process HTTP error responses.
 
1814
 
 
1815
    We don't really process the errors, quite the contrary
 
1816
    instead, we leave our Transport handle them.
 
1817
    """
 
1818
 
 
1819
    accepted_errors = [200,  # Ok
 
1820
                       206,  # Partial content
 
1821
                       400,
 
1822
                       403,
 
1823
                       404,  # Not found
 
1824
                       416,
 
1825
                       ]
 
1826
    """The error codes the caller will handle.
 
1827
 
 
1828
    This can be specialized in the request on a case-by case basis, but the
 
1829
    common cases are covered here.
 
1830
    """
 
1831
 
 
1832
    def http_response(self, request, response):
 
1833
        code, msg, hdrs = response.code, response.msg, response.info()
 
1834
 
 
1835
        if code not in self.accepted_errors:
 
1836
            response = self.parent.error('http', request, response,
 
1837
                                         code, msg, hdrs)
 
1838
        return response
 
1839
 
 
1840
    https_response = http_response
 
1841
 
 
1842
 
 
1843
class HTTPDefaultErrorHandler(urllib_request.HTTPDefaultErrorHandler):
 
1844
    """Translate common errors into Breezy Exceptions"""
 
1845
 
 
1846
    def http_error_default(self, req, fp, code, msg, hdrs):
 
1847
        if code == 403:
 
1848
            raise errors.TransportError(
 
1849
                'Server refuses to fulfill the request (403 Forbidden)'
 
1850
                ' for %s' % req.get_full_url())
 
1851
        else:
 
1852
            raise errors.InvalidHttpResponse(req.get_full_url(),
 
1853
                                             'Unable to handle http code %d: %s'
 
1854
                                             % (code, msg))
 
1855
 
 
1856
 
 
1857
class Opener(object):
 
1858
    """A wrapper around urllib_request.build_opener
 
1859
 
 
1860
    Daughter classes can override to build their own specific opener
 
1861
    """
 
1862
    # TODO: Provides hooks for daughter classes.
 
1863
 
 
1864
    def __init__(self,
 
1865
                 connection=ConnectionHandler,
 
1866
                 redirect=HTTPRedirectHandler,
 
1867
                 error=HTTPErrorProcessor,
 
1868
                 report_activity=None,
 
1869
                 ca_certs=None):
 
1870
        self._opener = urllib_request.build_opener(
 
1871
            connection(report_activity=report_activity, ca_certs=ca_certs),
 
1872
            redirect, error,
 
1873
            ProxyHandler(),
 
1874
            HTTPBasicAuthHandler(),
 
1875
            HTTPDigestAuthHandler(),
 
1876
            HTTPNegotiateAuthHandler(),
 
1877
            ProxyBasicAuthHandler(),
 
1878
            ProxyDigestAuthHandler(),
 
1879
            ProxyNegotiateAuthHandler(),
 
1880
            HTTPHandler,
 
1881
            HTTPSHandler,
 
1882
            HTTPDefaultErrorHandler,
 
1883
            )
 
1884
 
 
1885
        self.open = self._opener.open
 
1886
        if DEBUG >= 9:
 
1887
            # When dealing with handler order, it's easy to mess
 
1888
            # things up, the following will help understand which
 
1889
            # handler is used, when and for what.
 
1890
            import pprint
 
1891
            pprint.pprint(self._opener.__dict__)
49
1892
 
50
1893
 
51
1894
class HttpTransport(ConnectedTransport):
62
1905
    # httplib, which use print :(
63
1906
    _debuglevel = 0
64
1907
 
65
 
    _opener_class = Opener
66
 
 
67
1908
    def __init__(self, base, _from_transport=None, ca_certs=None):
68
1909
        """Set the base path where files will be stored."""
69
1910
        proto_match = re.match(r'^(https?)(\+\w+)?://', base)
71
1912
            raise AssertionError("not a http url: %r" % base)
72
1913
        self._unqualified_scheme = proto_match.group(1)
73
1914
        super(HttpTransport, self).__init__(
74
 
                base, _from_transport=_from_transport)
 
1915
            base, _from_transport=_from_transport)
75
1916
        self._medium = None
76
1917
        # range hint is handled dynamically throughout the life
77
1918
        # of the transport object. We start by trying multi-range
85
1926
            self._opener = _from_transport._opener
86
1927
        else:
87
1928
            self._range_hint = 'multi'
88
 
            self._opener = self._opener_class(
 
1929
            self._opener = Opener(
89
1930
                report_activity=self._report_activity, ca_certs=ca_certs)
90
1931
 
91
 
    def _perform(self, request):
92
 
        """Send the request to the server and handles common errors.
93
 
 
94
 
        :returns: urllib2 Response object
95
 
        """
 
1932
    def request(self, method, url, fields=None, headers=None, **urlopen_kw):
 
1933
        if fields is not None:
 
1934
            raise NotImplementedError(
 
1935
                'the fields argument is not yet supported')
 
1936
        body = urlopen_kw.pop('body', None)
 
1937
        if headers is None:
 
1938
            headers = {}
 
1939
        request = Request(method, url, body, headers)
 
1940
        request.follow_redirections = (urlopen_kw.pop('retries', 0) > 0)
 
1941
        if urlopen_kw:
 
1942
            raise NotImplementedError(
 
1943
                'unknown arguments: %r' % urlopen_kw.keys())
96
1944
        connection = self._get_connection()
97
1945
        if connection is not None:
98
1946
            # Give back shared info
126
1974
 
127
1975
        code = response.code
128
1976
        if (request.follow_redirections is False
129
 
            and code in (301, 302, 303, 307)):
 
1977
                and code in (301, 302, 303, 307)):
130
1978
            raise errors.RedirectRequested(request.get_full_url(),
131
1979
                                           request.redirected_to,
132
1980
                                           is_permanent=(code == 301))
135
1983
            trace.mutter('redirected from: %s to: %s' % (request.get_full_url(),
136
1984
                                                         request.redirected_to))
137
1985
 
138
 
        return response
 
1986
        class Urllib3LikeResponse(object):
 
1987
 
 
1988
            def __init__(self, actual):
 
1989
                self._actual = actual
 
1990
                self._data = None
 
1991
 
 
1992
            def getheader(self, name, default=None):
 
1993
                if self._actual.headers is None:
 
1994
                    raise http_client.ResponseNotReady()
 
1995
                if PY3:
 
1996
                    return self._actual.headers.get(name, default)
 
1997
                else:
 
1998
                    return self._actual.headers.getheader(name, default)
 
1999
 
 
2000
            def getheaders(self):
 
2001
                if self._actual.headers is None:
 
2002
                    raise http_client.ResponseNotReady()
 
2003
                return list(self._actual.headers.items())
 
2004
 
 
2005
            @property
 
2006
            def status(self):
 
2007
                return self._actual.code
 
2008
 
 
2009
            @property
 
2010
            def data(self):
 
2011
                if self._data is None:
 
2012
                    self._data = self._actual.read()
 
2013
                return self._data
 
2014
 
 
2015
            def read(self, amt=None):
 
2016
                return self._actual.read(amt)
 
2017
 
 
2018
            def readline(self, size=-1):
 
2019
                return self._actual.readline(size)
 
2020
 
 
2021
        return Urllib3LikeResponse(response)
139
2022
 
140
2023
    def disconnect(self):
141
2024
        connection = self._get_connection()
147
2030
        """
148
2031
        response = self._head(relpath)
149
2032
 
150
 
        code = response.code
151
 
        if code == 200: # "ok",
 
2033
        code = response.status
 
2034
        if code == 200:  # "ok",
152
2035
            return True
153
2036
        else:
154
2037
            return False
173
2056
        """
174
2057
        abspath = self._remote_path(relpath)
175
2058
        headers = {}
176
 
        accepted_errors = [200, 404]
177
2059
        if offsets or tail_amount:
178
2060
            range_header = self._attempted_range_header(offsets, tail_amount)
179
2061
            if range_header is not None:
180
 
                accepted_errors.append(206)
181
 
                accepted_errors.append(400)
182
 
                accepted_errors.append(416)
183
2062
                bytes = 'bytes=' + range_header
184
2063
                headers = {'Range': bytes}
185
2064
 
186
 
        request = Request('GET', abspath, None, headers,
187
 
                          accepted_errors=accepted_errors)
188
 
        response = self._perform(request)
 
2065
        response = self.request('GET', abspath, headers=headers)
189
2066
 
190
 
        code = response.code
191
 
        if code == 404: # not found
 
2067
        if response.status == 404:  # not found
192
2068
            raise errors.NoSuchFile(abspath)
193
 
        elif code in (400, 416):
 
2069
        elif response.status in (400, 416):
194
2070
            # We don't know which, but one of the ranges we specified was
195
2071
            # wrong.
196
2072
            raise errors.InvalidHttpRange(abspath, range_header,
197
 
                                          'Server return code %d' % code)
 
2073
                                          'Server return code %d' % response.status)
 
2074
        elif response.status not in (200, 206):
 
2075
            raise errors.InvalidHttpResponse(
 
2076
                abspath, 'Unexpected status %d' % response.status)
198
2077
 
199
 
        data = handle_response(abspath, code, response.info(), response)
200
 
        return code, data
 
2078
        data = handle_response(
 
2079
            abspath, response.status, response.getheader, response)
 
2080
        return response.status, data
201
2081
 
202
2082
    def _remote_path(self, relpath):
203
2083
        """See ConnectedTransport._remote_path.
287
2167
            coalesced = list(coalesced)
288
2168
            if 'http' in debug.debug_flags:
289
2169
                mutter('http readv of %s  offsets => %s collapsed %s',
290
 
                    relpath, len(offsets), len(coalesced))
 
2170
                       relpath, len(offsets), len(coalesced))
291
2171
 
292
2172
            # Cache the data read, but only until it's been used
293
2173
            data_map = {}
339
2219
                    errors.InvalidHttpRange, errors.HttpBoundaryMissing) as e:
340
2220
                mutter('Exception %r: %s during http._readv', e, e)
341
2221
                if (not isinstance(e, errors.ShortReadvError)
342
 
                    or retried_offset == cur_offset_and_size):
 
2222
                        or retried_offset == cur_offset_and_size):
343
2223
                    # We don't degrade the range hint for ShortReadvError since
344
2224
                    # they do not indicate a problem with the server ability to
345
2225
                    # handle ranges. Except when we fail to get back a required
387
2267
            ranges = []
388
2268
            for coal in coalesced:
389
2269
                if ((self._get_max_size > 0
390
 
                     and cumul + coal.length > self._get_max_size)
391
 
                    or len(ranges) >= max_ranges):
 
2270
                     and cumul + coal.length > self._get_max_size) or
 
2271
                        len(ranges) >= max_ranges):
392
2272
                    # Get that much and yield
393
2273
                    for c, rfile in get_and_yield(relpath, ranges):
394
2274
                        yield c, rfile
420
2300
        # an interface that allows streaming via POST when possible (and
421
2301
        # degrades to a local buffer when not).
422
2302
        abspath = self._remote_path('.bzr/smart')
423
 
        # We include 403 in accepted_errors so that send_http_smart_request can
424
 
        # handle a 403.  Otherwise a 403 causes an unhandled TransportError.
425
 
        response = self._perform(
426
 
            Request('POST', abspath, body_bytes,
427
 
                    {'Content-Type': 'application/octet-stream'},
428
 
                    accepted_errors=[200, 403]))
429
 
        code = response.code
430
 
        data = handle_response(abspath, code, response.info(), response)
 
2303
        response = self.request(
 
2304
            'POST', abspath, body=body_bytes,
 
2305
            headers={'Content-Type': 'application/octet-stream'})
 
2306
        if response.status not in (200, 403):
 
2307
            raise errors.InvalidHttpResponse(
 
2308
                url, 'Unexpected status %d' % response.status)
 
2309
        code = response.status
 
2310
        data = handle_response(
 
2311
            abspath, code, response.getheader, response)
431
2312
        return code, data
432
2313
 
433
2314
    def _head(self, relpath):
436
2317
        Performs the request and leaves callers handle the results.
437
2318
        """
438
2319
        abspath = self._remote_path(relpath)
439
 
        request = Request('HEAD', abspath,
440
 
                          accepted_errors=[200, 404])
441
 
        response = self._perform(request)
 
2320
        response = self.request('HEAD', abspath)
 
2321
        if response.status not in (200, 404):
 
2322
            raise errors.InvalidHttpResponse(
 
2323
                abspath, 'Unexpected status %d' % response.status)
442
2324
 
443
2325
        return response
444
2326
 
445
 
 
446
2327
        raise NotImplementedError(self._post)
447
2328
 
448
2329
    def put_file(self, relpath, f, mode=None):
487
2368
                'http cannot be the target of copy_to()')
488
2369
        else:
489
2370
            return super(HttpTransport, self).\
490
 
                    copy_to(relpaths, other, mode=mode, pb=pb)
 
2371
                copy_to(relpaths, other, mode=mode, pb=pb)
491
2372
 
492
2373
    def move(self, rel_from, rel_to):
493
2374
        """Move the item at rel_from to the location at rel_to"""
527
2408
        class BogusLock(object):
528
2409
            def __init__(self, path):
529
2410
                self.path = path
 
2411
 
530
2412
            def unlock(self):
531
2413
                pass
532
2414
        return BogusLock(relpath)
643
2525
                # user)
644
2526
                redir_scheme = parsed_target.scheme
645
2527
                new_url = self._unsplit_url(redir_scheme,
646
 
                    self._parsed_url.user,
647
 
                    self._parsed_url.password,
648
 
                    parsed_target.host, parsed_target.port,
649
 
                    target_path)
 
2528
                                            self._parsed_url.user,
 
2529
                                            self._parsed_url.password,
 
2530
                                            parsed_target.host, parsed_target.port,
 
2531
                                            target_path)
650
2532
                return transport.get_transport_from_url(new_url)
651
2533
        else:
652
2534
            # Redirected to a different protocol
653
2535
            new_url = self._unsplit_url(parsed_target.scheme,
654
 
                    parsed_target.user,
655
 
                    parsed_target.password,
656
 
                    parsed_target.host, parsed_target.port,
657
 
                    target_path)
 
2536
                                        parsed_target.user,
 
2537
                                        parsed_target.password,
 
2538
                                        parsed_target.host, parsed_target.port,
 
2539
                                        target_path)
658
2540
            return transport.get_transport_from_url(new_url)
659
2541
 
660
2542
 
762
2644
        features,
763
2645
        http_server,
764
2646
        )
765
 
    permutations = [(HttpTransport, http_server.HttpServer),]
 
2647
    permutations = [(HttpTransport, http_server.HttpServer), ]
766
2648
    if features.HTTPSServerFeature.available():
767
2649
        from breezy.tests import (
768
2650
            https_server,