~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/transport/http/_urllib2_wrappers.py

  • Committer: John Arbash Meinel
  • Author(s): Mark Hammond
  • Date: 2008-09-09 17:02:21 UTC
  • mto: This revision was merged to the branch mainline in revision 3697.
  • Revision ID: john@arbash-meinel.com-20080909170221-svim3jw2mrz0amp3
An updated transparent icon for bzr.

Show diffs side-by-side

added added

removed removed

Lines of Context:
1
 
# Copyright (C) 2006-2013, 2016 Canonical Ltd
 
1
# Copyright (C) 2006, 2007 Canonical Ltd
2
2
#
3
3
# This program is free software; you can redistribute it and/or modify
4
4
# it under the terms of the GNU General Public License as published by
12
12
#
13
13
# You should have received a copy of the GNU General Public License
14
14
# along with this program; if not, write to the Free Software
15
 
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
 
15
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
16
16
 
17
 
"""Implementation of urllib2 tailored to bzr needs
 
17
"""Implementaion of urllib2 tailored to bzr needs
18
18
 
19
19
This file complements the urllib2 class hierarchy with custom classes.
20
20
 
36
36
request (see AbstractHTTPHandler.do_open).
37
37
"""
38
38
 
39
 
from __future__ import absolute_import
40
 
 
41
39
DEBUG = 0
42
40
 
43
41
# FIXME: Oversimplifying, two kind of exceptions should be
48
46
# actual code more or less do that, tests should be written to
49
47
# ensure that.
50
48
 
51
 
import errno
52
49
import httplib
53
 
import os
 
50
import md5
 
51
import sha
54
52
import socket
55
53
import urllib
56
54
import urllib2
57
55
import urlparse
58
56
import re
59
 
import ssl
60
57
import sys
61
58
import time
62
59
 
65
62
    config,
66
63
    debug,
67
64
    errors,
68
 
    lazy_import,
69
 
    osutils,
70
65
    trace,
71
66
    transport,
72
67
    ui,
73
 
    urlutils,
74
 
)
75
 
 
76
 
try:
77
 
    _ = (ssl.match_hostname, ssl.CertificateError)
78
 
except AttributeError:
79
 
    # Provide fallbacks for python < 2.7.9
80
 
    def match_hostname(cert, host):
81
 
        trace.warning(
82
 
            '%s cannot be verified, https certificates verification is only'
83
 
            ' available for python versions >= 2.7.9' % (host,))
84
 
    ssl.match_hostname = match_hostname
85
 
    ssl.CertificateError = ValueError
86
 
 
87
 
 
88
 
# Note for packagers: if there is no package providing certs for your platform,
89
 
# the curl project produces http://curl.haxx.se/ca/cacert.pem weekly.
90
 
_ssl_ca_certs_known_locations = [
91
 
    u'/etc/ssl/certs/ca-certificates.crt',  # Ubuntu/debian/gentoo
92
 
    u'/etc/pki/tls/certs/ca-bundle.crt',  # Fedora/CentOS/RH
93
 
    u'/etc/ssl/ca-bundle.pem',  # OpenSuse
94
 
    u'/etc/ssl/cert.pem',  # OpenSuse
95
 
    u"/usr/local/share/certs/ca-root-nss.crt",  # FreeBSD
96
 
    # XXX: Needs checking, can't trust the interweb ;) -- vila 2012-01-25
97
 
    u'/etc/openssl/certs/ca-certificates.crt',  # Solaris
98
 
]
99
 
 
100
 
 
101
 
def default_ca_certs():
102
 
    if sys.platform == 'win32':
103
 
        return os.path.join(os.path.dirname(sys.executable), u"cacert.pem")
104
 
    elif sys.platform == 'darwin':
105
 
        # FIXME: Needs some default value for osx, waiting for osx installers
106
 
        # guys feedback -- vila 2012-01-25
107
 
        pass
108
 
    else:
109
 
        # Try known locations for friendly OSes providing the root certificates
110
 
        # without making them hard to use for any https client.
111
 
        for path in _ssl_ca_certs_known_locations:
112
 
            if os.path.exists(path):
113
 
                # First found wins
114
 
                return path
115
 
    # A default path that makes sense and will be mentioned in the error
116
 
    # presented to the user, even if not correct for all platforms
117
 
    return _ssl_ca_certs_known_locations[0]
118
 
 
119
 
 
120
 
def ca_certs_from_store(path):
121
 
    if not os.path.exists(path):
122
 
        raise ValueError("ca certs path %s does not exist" % path)
123
 
    return path
124
 
 
125
 
 
126
 
def cert_reqs_from_store(unicode_str):
127
 
    import ssl
128
 
    try:
129
 
        return {"required": ssl.CERT_REQUIRED,
130
 
                "none": ssl.CERT_NONE}[unicode_str]
131
 
    except KeyError:
132
 
        raise ValueError("invalid value %s" % unicode_str)
133
 
 
134
 
 
135
 
def default_ca_reqs():
136
 
    if sys.platform in ('win32', 'darwin'):
137
 
        # FIXME: Once we get a native access to root certificates there, this
138
 
        # won't needed anymore. See http://pad.lv/920455 -- vila 2012-02-15
139
 
        return u'none'
140
 
    else:
141
 
        return u'required'
142
 
 
143
 
opt_ssl_ca_certs = config.Option('ssl.ca_certs',
144
 
                                 from_unicode=ca_certs_from_store,
145
 
                                 default=default_ca_certs,
146
 
                                 invalid='warning',
147
 
                                 help="""\
148
 
Path to certification authority certificates to trust.
149
 
 
150
 
This should be a valid path to a bundle containing all root Certificate
151
 
Authorities used to verify an https server certificate.
152
 
 
153
 
Use ssl.cert_reqs=none to disable certificate verification.
154
 
""")
155
 
 
156
 
opt_ssl_cert_reqs = config.Option('ssl.cert_reqs',
157
 
                                  default=default_ca_reqs,
158
 
                                  from_unicode=cert_reqs_from_store,
159
 
                                  invalid='error',
160
 
                                  help="""\
161
 
Whether to require a certificate from the remote side. (default:required)
162
 
 
163
 
Possible values:
164
 
 * none: Certificates ignored
165
 
 * required: Certificates required and validated
166
 
""")
167
 
 
168
 
checked_kerberos = False
169
 
kerberos = None
170
 
 
171
 
 
172
 
class addinfourl(urllib2.addinfourl):
173
 
    '''Replacement addinfourl class compatible with python-2.7's xmlrpclib
174
 
 
175
 
    In python-2.7, xmlrpclib expects that the response object that it receives
176
 
    has a getheader method.  httplib.HTTPResponse provides this but
177
 
    urllib2.addinfourl does not.  Add the necessary functions here, ported to
178
 
    use the internal data structures of addinfourl.
179
 
    '''
180
 
 
181
 
    def getheader(self, name, default=None):
182
 
        if self.headers is None:
183
 
            raise httplib.ResponseNotReady()
184
 
        return self.headers.getheader(name, default)
185
 
 
186
 
    def getheaders(self):
187
 
        if self.headers is None:
188
 
            raise httplib.ResponseNotReady()
189
 
        return self.headers.items()
190
 
 
191
 
 
192
 
class _ReportingFileSocket(object):
193
 
 
194
 
    def __init__(self, filesock, report_activity=None):
195
 
        self.filesock = filesock
196
 
        self._report_activity = report_activity
197
 
 
198
 
    def report_activity(self, size, direction):
199
 
        if self._report_activity:
200
 
            self._report_activity(size, direction)
201
 
 
202
 
    def read(self, size=1):
203
 
        s = self.filesock.read(size)
204
 
        self.report_activity(len(s), 'read')
205
 
        return s
206
 
 
207
 
    def readline(self, size=-1):
208
 
        s = self.filesock.readline(size)
209
 
        self.report_activity(len(s), 'read')
210
 
        return s
211
 
 
212
 
    def __getattr__(self, name):
213
 
        return getattr(self.filesock, name)
214
 
 
215
 
 
216
 
class _ReportingSocket(object):
217
 
 
218
 
    def __init__(self, sock, report_activity=None):
 
68
    )
 
69
 
 
70
 
 
71
class _BufferedMakefileSocket(object):
 
72
 
 
73
    def __init__(self, sock):
219
74
        self.sock = sock
220
 
        self._report_activity = report_activity
221
 
 
222
 
    def report_activity(self, size, direction):
223
 
        if self._report_activity:
224
 
            self._report_activity(size, direction)
225
 
 
226
 
    def sendall(self, s, *args):
227
 
        self.sock.sendall(s, *args)
228
 
        self.report_activity(len(s), 'write')
229
 
 
230
 
    def recv(self, *args):
231
 
        s = self.sock.recv(*args)
232
 
        self.report_activity(len(s), 'read')
233
 
        return s
234
75
 
235
76
    def makefile(self, mode='r', bufsize=-1):
236
 
        # httplib creates a fileobject that doesn't do buffering, which
237
 
        # makes fp.readline() very expensive because it only reads one byte
238
 
        # at a time.  So we wrap the socket in an object that forces
239
 
        # sock.makefile to make a buffered file.
240
 
        fsock = self.sock.makefile(mode, 65536)
241
 
        # And wrap that into a reporting kind of fileobject
242
 
        return _ReportingFileSocket(fsock, self._report_activity)
 
77
        return self.sock.makefile(mode, 65536)
243
78
 
244
79
    def __getattr__(self, name):
245
80
        return getattr(self.sock, name)
254
89
    """
255
90
 
256
91
    # Some responses have bodies in which we have no interest
257
 
    _body_ignored_responses = [301,302, 303, 307, 400, 401, 403, 404, 501]
 
92
    _body_ignored_responses = [301,302, 303, 307, 401, 403, 404]
258
93
 
259
94
    # in finish() below, we may have to discard several MB in the worst
260
95
    # case. To avoid buffering that much, we read and discard by chunks
262
97
    # 8k chunks should be fine.
263
98
    _discarded_buf_size = 8192
264
99
 
 
100
    def __init__(self, sock, *args, **kwargs):
 
101
        # httplib creates a fileobject that doesn't do buffering, which
 
102
        # makes fp.readline() very expensive because it only reads one byte
 
103
        # at a time.  So we wrap the socket in an object that forces
 
104
        # sock.makefile to make a buffered file.
 
105
        sock = _BufferedMakefileSocket(sock)
 
106
        httplib.HTTPResponse.__init__(self, sock, *args, **kwargs)
 
107
 
265
108
    def begin(self):
266
109
        """Begin to read the response from the server.
267
110
 
336
179
    # we want to warn. But not below a given thresold.
337
180
    _range_warning_thresold = 1024 * 1024
338
181
 
339
 
    def __init__(self, report_activity=None):
 
182
    def __init__(self):
340
183
        self._response = None
341
 
        self._report_activity = report_activity
342
184
        self._ranges_received_whole_file = None
343
185
 
344
186
    def _mutter_connect(self):
355
197
    def cleanup_pipe(self):
356
198
        """Read the remaining bytes of the last response if any."""
357
199
        if self._response is not None:
358
 
            try:
359
 
                pending = self._response.finish()
360
 
                # Warn the user (once)
361
 
                if (self._ranges_received_whole_file is None
362
 
                    and self._response.status == 200
363
 
                    and pending and pending > self._range_warning_thresold
364
 
                    ):
365
 
                    self._ranges_received_whole_file = True
366
 
                    trace.warning(
367
 
                        'Got a 200 response when asking for multiple ranges,'
368
 
                        ' does your server at %s:%s support range requests?',
369
 
                        self.host, self.port)
370
 
            except socket.error, e:
371
 
                # It's conceivable that the socket is in a bad state here
372
 
                # (including some test cases) and in this case, it doesn't need
373
 
                # cleaning anymore, so no need to fail, we just get rid of the
374
 
                # socket and let callers reconnect
375
 
                if (len(e.args) == 0
376
 
                    or e.args[0] not in (errno.ECONNRESET, errno.ECONNABORTED)):
377
 
                    raise
378
 
                self.close()
 
200
            pending = self._response.finish()
 
201
            # Warn the user (once)
 
202
            if (self._ranges_received_whole_file is None
 
203
                and self._response.status == 200
 
204
                and pending and pending > self._range_warning_thresold
 
205
                ):
 
206
                self._ranges_received_whole_file = True
 
207
                trace.warning(
 
208
                    'Got a 200 response when asking for multiple ranges,'
 
209
                    ' does your server at %s:%s support range requests?',
 
210
                    self.host, self.port)
379
211
            self._response = None
380
212
        # Preserve our preciousss
381
213
        sock = self.sock
382
214
        self.sock = None
383
 
        # Let httplib.HTTPConnection do its housekeeping
 
215
        # Let httplib.HTTPConnection do its housekeeping 
384
216
        self.close()
385
217
        # Restore our preciousss
386
218
        self.sock = sock
387
219
 
388
 
    def _wrap_socket_for_reporting(self, sock):
389
 
        """Wrap the socket before anybody use it."""
390
 
        self.sock = _ReportingSocket(sock, self._report_activity)
391
 
 
392
220
 
393
221
class HTTPConnection(AbstractHTTPConnection, httplib.HTTPConnection):
394
222
 
395
223
    # XXX: Needs refactoring at the caller level.
396
 
    def __init__(self, host, port=None, proxied_host=None,
397
 
                 report_activity=None, ca_certs=None):
398
 
        AbstractHTTPConnection.__init__(self, report_activity=report_activity)
 
224
    def __init__(self, host, port=None, proxied_host=None):
 
225
        AbstractHTTPConnection.__init__(self)
399
226
        # Use strict=True since we don't support HTTP/0.9
400
227
        httplib.HTTPConnection.__init__(self, host, port, strict=True)
401
228
        self.proxied_host = proxied_host
402
 
        # ca_certs is ignored, it's only relevant for https
403
229
 
404
230
    def connect(self):
405
231
        if 'http' in debug.debug_flags:
406
232
            self._mutter_connect()
407
233
        httplib.HTTPConnection.connect(self)
408
 
        self._wrap_socket_for_reporting(self.sock)
409
 
 
410
 
 
 
234
 
 
235
 
 
236
# FIXME: Should test for ssl availability
411
237
class HTTPSConnection(AbstractHTTPConnection, httplib.HTTPSConnection):
412
238
 
413
239
    def __init__(self, host, port=None, key_file=None, cert_file=None,
414
 
                 proxied_host=None,
415
 
                 report_activity=None, ca_certs=None):
416
 
        AbstractHTTPConnection.__init__(self, report_activity=report_activity)
 
240
                 proxied_host=None):
 
241
        AbstractHTTPConnection.__init__(self)
417
242
        # Use strict=True since we don't support HTTP/0.9
418
243
        httplib.HTTPSConnection.__init__(self, host, port,
419
244
                                         key_file, cert_file, strict=True)
420
245
        self.proxied_host = proxied_host
421
 
        self.ca_certs = ca_certs
422
246
 
423
247
    def connect(self):
424
248
        if 'http' in debug.debug_flags:
425
249
            self._mutter_connect()
426
250
        httplib.HTTPConnection.connect(self)
427
 
        self._wrap_socket_for_reporting(self.sock)
428
251
        if self.proxied_host is None:
429
252
            self.connect_to_origin()
430
253
 
431
254
    def connect_to_origin(self):
432
 
        # FIXME JRV 2011-12-18: Use location config here?
433
 
        config_stack = config.GlobalStack()
434
 
        cert_reqs = config_stack.get('ssl.cert_reqs')
435
 
        if self.proxied_host is not None:
436
 
            host = self.proxied_host.split(":", 1)[0]
437
 
        else:
438
 
            host = self.host
439
 
        if cert_reqs == ssl.CERT_NONE:
440
 
            ui.ui_factory.show_user_warning('not_checking_ssl_cert', host=host)
441
 
            ui.ui_factory.suppressed_warnings.add('not_checking_ssl_cert')
442
 
            ca_certs = None
443
 
        else:
444
 
            if self.ca_certs is None:
445
 
                ca_certs = config_stack.get('ssl.ca_certs')
446
 
            else:
447
 
                ca_certs = self.ca_certs
448
 
            if ca_certs is None:
449
 
                trace.warning(
450
 
                    "No valid trusted SSL CA certificates file set. See "
451
 
                    "'bzr help ssl.ca_certs' for more information on setting "
452
 
                    "trusted CAs.")
453
 
        try:
454
 
            ssl_sock = ssl.wrap_socket(
455
 
                self.sock, self.key_file, self.cert_file,
456
 
                cert_reqs=cert_reqs, ca_certs=ca_certs)
457
 
        except ssl.SSLError:
458
 
            trace.note(
459
 
                "\n"
460
 
                "See `bzr help ssl.ca_certs` for how to specify trusted CA"
461
 
                "certificates.\n"
462
 
                "Pass -Ossl.cert_reqs=none to disable certificate "
463
 
                "verification entirely.\n")
464
 
            raise
465
 
        if cert_reqs == ssl.CERT_REQUIRED:
466
 
            peer_cert = ssl_sock.getpeercert()
467
 
            ssl.match_hostname(peer_cert, host)
468
 
 
469
 
        # Wrap the ssl socket before anybody use it
470
 
        self._wrap_socket_for_reporting(ssl_sock)
 
255
        ssl = socket.ssl(self.sock, self.key_file, self.cert_file)
 
256
        self.sock = httplib.FakeSocket(self.sock, ssl)
471
257
 
472
258
 
473
259
class Request(urllib2.Request):
511
297
 
512
298
    def set_proxy(self, proxy, type):
513
299
        """Set the proxy and remember the proxied host."""
514
 
        host, port = urllib.splitport(self.get_host())
515
 
        if port is None:
516
 
            # We need to set the default port ourselves way before it gets set
517
 
            # in the HTTP[S]Connection object at build time.
518
 
            if self.type == 'https':
519
 
                conn_class = HTTPSConnection
520
 
            else:
521
 
                conn_class = HTTPConnection
522
 
            port = conn_class.default_port
523
 
        self.proxied_host = '%s:%s' % (host, port)
 
300
        self.proxied_host = self.get_host()
524
301
        urllib2.Request.set_proxy(self, proxy, type)
525
 
        # When urllib2 makes a https request with our wrapper code and a proxy,
526
 
        # it sets Host to the https proxy, not the host we want to talk to.
527
 
        # I'm fairly sure this is our fault, but what is the cause is an open
528
 
        # question. -- Robert Collins May 8 2010.
529
 
        self.add_unredirected_header('Host', self.proxied_host)
530
302
 
531
303
 
532
304
class _ConnectRequest(Request):
533
305
 
534
306
    def __init__(self, request):
535
307
        """Constructor
536
 
 
 
308
        
537
309
        :param request: the first request sent to the proxied host, already
538
310
            processed by the opener (i.e. proxied_host is already set).
539
311
        """
573
345
 
574
346
    handler_order = 1000 # after all pre-processings
575
347
 
576
 
    def __init__(self, report_activity=None, ca_certs=None):
577
 
        self._report_activity = report_activity
578
 
        self.ca_certs = ca_certs
579
 
 
580
348
    def create_connection(self, request, http_connection_class):
581
349
        host = request.get_host()
582
350
        if not host:
588
356
        # request is made)
589
357
        try:
590
358
            connection = http_connection_class(
591
 
                host, proxied_host=request.proxied_host,
592
 
                report_activity=self._report_activity,
593
 
                ca_certs=self.ca_certs)
 
359
                host, proxied_host=request.proxied_host)
594
360
        except httplib.InvalidURL, exception:
595
361
            # There is only one occurrence of InvalidURL in httplib
596
362
            raise errors.InvalidURL(request.get_full_url(),
709
475
                        request.get_full_url(),
710
476
                        'Bad status line received',
711
477
                        orig_error=exc_val)
712
 
                elif (isinstance(exc_val, socket.error) and len(exc_val.args)
713
 
                      and exc_val.args[0] in (errno.ECONNRESET, 10053, 10054)):
714
 
                      # 10053 == WSAECONNABORTED
715
 
                      # 10054 == WSAECONNRESET
716
 
                    raise errors.ConnectionReset(
717
 
                        "Connection lost while sending request.")
718
478
                else:
719
479
                    # All other exception are considered connection related.
720
480
 
769
529
                                     headers)
770
530
            if 'http' in debug.debug_flags:
771
531
                trace.mutter('> %s %s' % (method, url))
772
 
                hdrs = []
773
 
                for k,v in headers.iteritems():
774
 
                    # People are often told to paste -Dhttp output to help
775
 
                    # debug. Don't compromise credentials.
776
 
                    if k in ('Authorization', 'Proxy-Authorization'):
777
 
                        v = '<masked>'
778
 
                    hdrs.append('%s: %s' % (k, v))
 
532
                hdrs = ['%s: %s' % (k, v) for k,v in headers.items()]
779
533
                trace.mutter('> ' + '\n> '.join(hdrs) + '\n')
780
534
            if self._debuglevel >= 1:
781
535
                print 'Request sent: [%r] from (%s)' \
782
536
                    % (request, request.connection.sock.getsockname())
783
537
            response = connection.getresponse()
784
538
            convert_to_addinfourl = True
785
 
        except (ssl.SSLError, ssl.CertificateError):
786
 
            # Something is wrong with either the certificate or the hostname,
787
 
            # re-trying won't help
788
 
            raise
789
539
        except (socket.gaierror, httplib.BadStatusLine, httplib.UnknownProtocol,
790
540
                socket.error, httplib.HTTPException):
791
541
            response = self.retry_or_raise(http_class, request, first_try)
814
564
            r = response
815
565
            r.recv = r.read
816
566
            fp = socket._fileobject(r, bufsize=65536)
817
 
            resp = addinfourl(fp, r.msg, req.get_full_url())
 
567
            resp = urllib2.addinfourl(fp, r.msg, req.get_full_url())
818
568
            resp.code = r.status
819
569
            resp.msg = r.reason
820
570
            resp.version = r.version
874
624
            connect = _ConnectRequest(request)
875
625
            response = self.parent.open(connect)
876
626
            if response.code != 200:
877
 
                raise errors.ConnectionError("Can't connect to %s via proxy %s" % (
 
627
                raise ConnectionError("Can't connect to %s via proxy %s" % (
878
628
                        connect.proxied_host, self.host))
879
629
            # Housekeeping
880
630
            connection.cleanup_pipe()
881
 
            # Establish the connection encryption
 
631
            # Establish the connection encryption 
882
632
            connection.connect_to_origin()
883
633
            # Propagate the connection to the original request
884
634
            request.connection = connection
1031
781
                print 'Will unbind %s_open for %r' % (type, proxy)
1032
782
            delattr(self, '%s_open' % type)
1033
783
 
1034
 
        def bind_scheme_request(proxy, scheme):
1035
 
            if proxy is None:
1036
 
                return
1037
 
            scheme_request = scheme + '_request'
1038
 
            if self._debuglevel >= 3:
1039
 
                print 'Will bind %s for %r' % (scheme_request, proxy)
1040
 
            setattr(self, scheme_request,
1041
 
                lambda request: self.set_proxy(request, scheme))
1042
784
        # We are interested only by the http[s] proxies
1043
785
        http_proxy = self.get_proxy_env_var('http')
1044
 
        bind_scheme_request(http_proxy, 'http')
1045
786
        https_proxy = self.get_proxy_env_var('https')
1046
 
        bind_scheme_request(https_proxy, 'https')
 
787
 
 
788
        if http_proxy is not None:
 
789
            if self._debuglevel >= 3:
 
790
                print 'Will bind http_request for %r' % http_proxy
 
791
            setattr(self, 'http_request',
 
792
                    lambda request: self.set_proxy(request, 'http'))
 
793
 
 
794
        if https_proxy is not None:
 
795
            if self._debuglevel >= 3:
 
796
                print 'Will bind http_request for %r' % https_proxy
 
797
            setattr(self, 'https_request',
 
798
                    lambda request: self.set_proxy(request, 'https'))
1047
799
 
1048
800
    def get_proxy_env_var(self, name, default_to='all'):
1049
801
        """Get a proxy env var.
1064
816
        return None
1065
817
 
1066
818
    def proxy_bypass(self, host):
1067
 
        """Check if host should be proxied or not.
1068
 
 
1069
 
        :returns: True to skip the proxy, False otherwise.
1070
 
        """
 
819
        """Check if host should be proxied or not"""
1071
820
        no_proxy = self.get_proxy_env_var('no', default_to=None)
1072
 
        bypass = self.evaluate_proxy_bypass(host, no_proxy)
1073
 
        if bypass is None:
1074
 
            # Nevertheless, there are platform-specific ways to
1075
 
            # ignore proxies...
1076
 
            return urllib.proxy_bypass(host)
1077
 
        else:
1078
 
            return bypass
1079
 
 
1080
 
    def evaluate_proxy_bypass(self, host, no_proxy):
1081
 
        """Check the host against a comma-separated no_proxy list as a string.
1082
 
 
1083
 
        :param host: ``host:port`` being requested
1084
 
 
1085
 
        :param no_proxy: comma-separated list of hosts to access directly.
1086
 
 
1087
 
        :returns: True to skip the proxy, False not to, or None to
1088
 
            leave it to urllib.
1089
 
        """
1090
821
        if no_proxy is None:
1091
 
            # All hosts are proxied
1092
822
            return False
1093
823
        hhost, hport = urllib.splitport(host)
1094
824
        # Does host match any of the domains mentioned in
1096
826
        # are fuzzy (to say the least). We try to allow most
1097
827
        # commonly seen values.
1098
828
        for domain in no_proxy.split(','):
1099
 
            domain = domain.strip()
1100
 
            if domain == '':
1101
 
                continue
1102
829
            dhost, dport = urllib.splitport(domain)
1103
830
            if hport == dport or dport is None:
1104
831
                # Protect glob chars
1107
834
                dhost = dhost.replace("?", r".")
1108
835
                if re.match(dhost, hhost, re.IGNORECASE):
1109
836
                    return True
1110
 
        # Nothing explicitly avoid the host
1111
 
        return None
 
837
        # Nevertheless, there are platform-specific ways to
 
838
        # ignore proxies...
 
839
        return urllib.proxy_bypass(host)
1112
840
 
1113
841
    def set_proxy(self, request, type):
1114
842
        if self.proxy_bypass(request.get_host()):
1121
849
        # grok user:password@host:port as well as
1122
850
        # http://user:password@host:port
1123
851
 
1124
 
        parsed_url = transport.ConnectedTransport._split_url(proxy)
1125
 
        if not parsed_url.host:
1126
 
            raise errors.InvalidURL(proxy, 'No host component')
 
852
        (scheme, user, password,
 
853
         host, port, path) = transport.ConnectedTransport._split_url(proxy)
1127
854
 
1128
855
        if request.proxy_auth == {}:
1129
856
            # No proxy auth parameter are available, we are handling the first
1130
857
            # proxied request, intialize.  scheme (the authentication scheme)
1131
858
            # and realm will be set by the AuthHandler
1132
859
            request.proxy_auth = {
1133
 
                                  'host': parsed_url.host,
1134
 
                                  'port': parsed_url.port,
1135
 
                                  'user': parsed_url.user,
1136
 
                                  'password': parsed_url.password,
1137
 
                                  'protocol': parsed_url.scheme,
 
860
                                  'host': host, 'port': port,
 
861
                                  'user': user, 'password': password,
 
862
                                  'protocol': scheme,
1138
863
                                   # We ignore path since we connect to a proxy
1139
864
                                  'path': None}
1140
 
        if parsed_url.port is None:
1141
 
            phost = parsed_url.host
 
865
        if port is None:
 
866
            phost = host
1142
867
        else:
1143
 
            phost = parsed_url.host + ':%d' % parsed_url.port
 
868
            phost = host + ':%d' % port
1144
869
        request.set_proxy(phost, type)
1145
870
        if self._debuglevel >= 3:
1146
871
            print 'set_proxy: proxy set to %s://%s' % (type, phost)
1154
879
    preventively set authentication headers after the first
1155
880
    successful authentication.
1156
881
 
1157
 
    This can be used for http and proxy, as well as for basic, negotiate and
 
882
    This can be used for http and proxy, as well as for basic and
1158
883
    digest authentications.
1159
884
 
1160
885
    This provides an unified interface for all authentication handlers
1185
910
      successful and the request authentication parameters have been updated.
1186
911
    """
1187
912
 
1188
 
    scheme = None
1189
 
    """The scheme as it appears in the server header (lower cased)"""
1190
 
 
1191
913
    _max_retry = 3
1192
914
    """We don't want to retry authenticating endlessly"""
1193
915
 
1194
 
    requires_username = True
1195
 
    """Whether the auth mechanism requires a username."""
1196
 
 
1197
916
    # The following attributes should be defined by daughter
1198
917
    # classes:
1199
918
    # - auth_required_header:  the header received from the server
1205
924
        # in such a cycle by default.
1206
925
        self._retry_count = None
1207
926
 
1208
 
    def _parse_auth_header(self, server_header):
1209
 
        """Parse the authentication header.
1210
 
 
1211
 
        :param server_header: The value of the header sent by the server
1212
 
            describing the authenticaion request.
1213
 
 
1214
 
        :return: A tuple (scheme, remainder) scheme being the first word in the
1215
 
            given header (lower cased), remainder may be None.
1216
 
        """
1217
 
        try:
1218
 
            scheme, remainder = server_header.split(None, 1)
1219
 
        except ValueError:
1220
 
            scheme = server_header
1221
 
            remainder = None
1222
 
        return (scheme.lower(), remainder)
1223
 
 
1224
927
    def update_auth(self, auth, key, value):
1225
928
        """Update a value in auth marking the auth as modified if needed"""
1226
929
        old_value = auth.get(key, None)
1245
948
                # Let's be ready for next round
1246
949
                self._retry_count = None
1247
950
                return None
1248
 
        server_headers = headers.getheaders(self.auth_required_header)
1249
 
        if not server_headers:
 
951
        server_header = headers.get(self.auth_required_header, None)
 
952
        if server_header is None:
1250
953
            # The http error MUST have the associated
1251
954
            # header. This must never happen in production code.
1252
955
            raise KeyError('%s not found' % self.auth_required_header)
1253
956
 
1254
957
        auth = self.get_auth(request)
 
958
        if auth.get('user', None) is None:
 
959
            # Without a known user, we can't authenticate
 
960
            return None
 
961
 
1255
962
        auth['modified'] = False
1256
 
        # Put some common info in auth if the caller didn't
1257
 
        if auth.get('path', None) is None:
1258
 
            parsed_url = urlutils.URL.from_string(request.get_full_url())
1259
 
            self.update_auth(auth, 'protocol', parsed_url.scheme)
1260
 
            self.update_auth(auth, 'host', parsed_url.host)
1261
 
            self.update_auth(auth, 'port', parsed_url.port)
1262
 
            self.update_auth(auth, 'path', parsed_url.path)
1263
 
        # FIXME: the auth handler should be selected at a single place instead
1264
 
        # of letting all handlers try to match all headers, but the current
1265
 
        # design doesn't allow a simple implementation.
1266
 
        for server_header in server_headers:
1267
 
            # Several schemes can be proposed by the server, try to match each
1268
 
            # one in turn
1269
 
            matching_handler = self.auth_match(server_header, auth)
1270
 
            if matching_handler:
1271
 
                # auth_match may have modified auth (by adding the
1272
 
                # password or changing the realm, for example)
1273
 
                if (request.get_header(self.auth_header, None) is not None
1274
 
                    and not auth['modified']):
1275
 
                    # We already tried that, give up
1276
 
                    return None
1277
 
 
1278
 
                # Only the most secure scheme proposed by the server should be
1279
 
                # used, since the handlers use 'handler_order' to describe that
1280
 
                # property, the first handler tried takes precedence, the
1281
 
                # others should not attempt to authenticate if the best one
1282
 
                # failed.
1283
 
                best_scheme = auth.get('best_scheme', None)
1284
 
                if best_scheme is None:
1285
 
                    # At that point, if current handler should doesn't succeed
1286
 
                    # the credentials are wrong (or incomplete), but we know
1287
 
                    # that the associated scheme should be used.
1288
 
                    best_scheme = auth['best_scheme'] = self.scheme
1289
 
                if  best_scheme != self.scheme:
1290
 
                    continue
1291
 
 
1292
 
                if self.requires_username and auth.get('user', None) is None:
1293
 
                    # Without a known user, we can't authenticate
1294
 
                    return None
1295
 
 
1296
 
                # Housekeeping
1297
 
                request.connection.cleanup_pipe()
1298
 
                # Retry the request with an authentication header added
1299
 
                response = self.parent.open(request)
1300
 
                if response:
1301
 
                    self.auth_successful(request, response)
1302
 
                return response
 
963
        if self.auth_match(server_header, auth):
 
964
            # auth_match may have modified auth (by adding the
 
965
            # password or changing the realm, for example)
 
966
            if (request.get_header(self.auth_header, None) is not None
 
967
                and not auth['modified']):
 
968
                # We already tried that, give up
 
969
                return None
 
970
 
 
971
            # Housekeeping
 
972
            request.connection.cleanup_pipe()
 
973
            response = self.parent.open(request)
 
974
            if response:
 
975
                self.auth_successful(request, response)
 
976
            return response
1303
977
        # We are not qualified to handle the authentication.
1304
978
        # Note: the authentication error handling will try all
1305
979
        # available handlers. If one of them authenticates
1325
999
        (digest's nonce is an example, digest's nonce_count is a
1326
1000
        *counter-example*). Such parameters must be updated by
1327
1001
        using the update_auth() method.
1328
 
 
 
1002
        
1329
1003
        :param header: The authentication header sent by the server.
1330
1004
        :param auth: The auth parameters already known. They may be
1331
1005
             updated.
1355
1029
        self._retry_count = None
1356
1030
 
1357
1031
    def get_user_password(self, auth):
1358
 
        """Ask user for a password if none is already available.
1359
 
 
1360
 
        :param auth: authentication info gathered so far (from the initial url
1361
 
            and then during dialog with the server).
1362
 
        """
 
1032
        """Ask user for a password if none is already available."""
1363
1033
        auth_conf = config.AuthenticationConfig()
1364
 
        user = auth.get('user', None)
1365
 
        password = auth.get('password', None)
 
1034
        user = auth['user']
 
1035
        password = auth['password']
1366
1036
        realm = auth['realm']
1367
 
        port = auth.get('port', None)
1368
1037
 
1369
1038
        if user is None:
1370
 
            user = auth_conf.get_user(auth['protocol'], auth['host'],
1371
 
                                      port=port, path=auth['path'],
1372
 
                                      realm=realm, ask=True,
1373
 
                                      prompt=self.build_username_prompt(auth))
1374
 
        if user is not None and password is None:
 
1039
            user = auth.get_user(auth['protocol'], auth['host'],
 
1040
                                 port=auth['port'], path=auth['path'],
 
1041
                                 realm=realm)
 
1042
            if user is None:
 
1043
                # Default to local user
 
1044
                user = getpass.getuser()
 
1045
 
 
1046
        if password is None:
1375
1047
            password = auth_conf.get_password(
1376
 
                auth['protocol'], auth['host'], user,
1377
 
                port=port,
 
1048
                auth['protocol'], auth['host'], user, port=auth['port'],
1378
1049
                path=auth['path'], realm=realm,
1379
1050
                prompt=self.build_password_prompt(auth))
1380
1051
 
1391
1062
        user. The daughter classes should implements a public
1392
1063
        build_password_prompt using this method.
1393
1064
        """
1394
 
        prompt = u'%s' % auth['protocol'].upper() + u' %(user)s@%(host)s'
1395
 
        realm = auth['realm']
1396
 
        if realm is not None:
1397
 
            prompt += u", Realm: '%s'" % realm.decode('utf8')
1398
 
        prompt += u' password'
1399
 
        return prompt
1400
 
 
1401
 
    def _build_username_prompt(self, auth):
1402
 
        """Build a prompt taking the protocol used into account.
1403
 
 
1404
 
        The AuthHandler is used by http and https, we want that information in
1405
 
        the prompt, so we build the prompt from the authentication dict which
1406
 
        contains all the needed parts.
1407
 
 
1408
 
        Also, http and proxy AuthHandlers present different prompts to the
1409
 
        user. The daughter classes should implements a public
1410
 
        build_username_prompt using this method.
1411
 
        """
1412
 
        prompt = u'%s' % auth['protocol'].upper() + u' %(host)s'
1413
 
        realm = auth['realm']
1414
 
        if realm is not None:
1415
 
            prompt += u", Realm: '%s'" % realm.decode('utf8')
1416
 
        prompt += u' username'
 
1065
        prompt = '%s' % auth['protocol'].upper() + ' %(user)s@%(host)s'
 
1066
        realm = auth['realm']
 
1067
        if realm is not None:
 
1068
            prompt += ", Realm: '%s'" % realm
 
1069
        prompt += ' password'
1417
1070
        return prompt
1418
1071
 
1419
1072
    def http_request(self, request):
1426
1079
    https_request = http_request # FIXME: Need test
1427
1080
 
1428
1081
 
1429
 
class NegotiateAuthHandler(AbstractAuthHandler):
1430
 
    """A authentication handler that handles WWW-Authenticate: Negotiate.
1431
 
 
1432
 
    At the moment this handler supports just Kerberos. In the future,
1433
 
    NTLM support may also be added.
1434
 
    """
1435
 
 
1436
 
    scheme = 'negotiate'
1437
 
    handler_order = 480
1438
 
    requires_username = False
1439
 
 
1440
 
    def auth_match(self, header, auth):
1441
 
        scheme, raw_auth = self._parse_auth_header(header)
1442
 
        if scheme != self.scheme:
1443
 
            return False
1444
 
        self.update_auth(auth, 'scheme', scheme)
1445
 
        resp = self._auth_match_kerberos(auth)
1446
 
        if resp is None:
1447
 
            return False
1448
 
        # Optionally should try to authenticate using NTLM here
1449
 
        self.update_auth(auth, 'negotiate_response', resp)
1450
 
        return True
1451
 
 
1452
 
    def _auth_match_kerberos(self, auth):
1453
 
        """Try to create a GSSAPI response for authenticating against a host."""
1454
 
        global kerberos, checked_kerberos
1455
 
        if kerberos is None and not checked_kerberos:
1456
 
            try:
1457
 
                import kerberos
1458
 
            except ImportError:
1459
 
                kerberos = None
1460
 
            checked_kerberos = True
1461
 
        if kerberos is None:
1462
 
            return None
1463
 
        ret, vc = kerberos.authGSSClientInit("HTTP@%(host)s" % auth)
1464
 
        if ret < 1:
1465
 
            trace.warning('Unable to create GSSAPI context for %s: %d',
1466
 
                auth['host'], ret)
1467
 
            return None
1468
 
        ret = kerberos.authGSSClientStep(vc, "")
1469
 
        if ret < 0:
1470
 
            trace.mutter('authGSSClientStep failed: %d', ret)
1471
 
            return None
1472
 
        return kerberos.authGSSClientResponse(vc)
1473
 
 
1474
 
    def build_auth_header(self, auth, request):
1475
 
        return "Negotiate %s" % auth['negotiate_response']
1476
 
 
1477
 
    def auth_params_reusable(self, auth):
1478
 
        # If the auth scheme is known, it means a previous
1479
 
        # authentication was successful, all information is
1480
 
        # available, no further checks are needed.
1481
 
        return (auth.get('scheme', None) == 'negotiate' and
1482
 
                auth.get('negotiate_response', None) is not None)
1483
 
 
1484
 
 
1485
1082
class BasicAuthHandler(AbstractAuthHandler):
1486
1083
    """A custom basic authentication handler."""
1487
1084
 
1488
 
    scheme = 'basic'
1489
1085
    handler_order = 500
 
1086
 
1490
1087
    auth_regexp = re.compile('realm="([^"]*)"', re.I)
1491
1088
 
1492
1089
    def build_auth_header(self, auth, request):
1494
1091
        auth_header = 'Basic ' + raw.encode('base64').strip()
1495
1092
        return auth_header
1496
1093
 
1497
 
    def extract_realm(self, header_value):
1498
 
        match = self.auth_regexp.search(header_value)
1499
 
        realm = None
1500
 
        if match:
1501
 
            realm = match.group(1)
1502
 
        return match, realm
1503
 
 
1504
1094
    def auth_match(self, header, auth):
1505
 
        scheme, raw_auth = self._parse_auth_header(header)
1506
 
        if scheme != self.scheme:
 
1095
        scheme, raw_auth = header.split(None, 1)
 
1096
        scheme = scheme.lower()
 
1097
        if scheme != 'basic':
1507
1098
            return False
1508
1099
 
1509
 
        match, realm = self.extract_realm(raw_auth)
 
1100
        match = self.auth_regexp.search(raw_auth)
1510
1101
        if match:
 
1102
            realm = match.groups()
 
1103
            if scheme != 'basic':
 
1104
                return False
 
1105
 
1511
1106
            # Put useful info into auth
1512
1107
            self.update_auth(auth, 'scheme', scheme)
1513
1108
            self.update_auth(auth, 'realm', realm)
1514
 
            if (auth.get('user', None) is None
1515
 
                or auth.get('password', None) is None):
 
1109
            if auth['user'] is None or auth['password'] is None:
1516
1110
                user, password = self.get_user_password(auth)
1517
1111
                self.update_auth(auth, 'user', user)
1518
1112
                self.update_auth(auth, 'password', password)
1529
1123
    H = None
1530
1124
    KD = None
1531
1125
    if algorithm == 'MD5':
1532
 
        H = lambda x: osutils.md5(x).hexdigest()
 
1126
        H = lambda x: md5.new(x).hexdigest()
1533
1127
    elif algorithm == 'SHA':
1534
 
        H = osutils.sha_string
 
1128
        H = lambda x: sha.new(x).hexdigest()
1535
1129
    if H is not None:
1536
1130
        KD = lambda secret, data: H("%s:%s" % (secret, data))
1537
1131
    return H, KD
1540
1134
def get_new_cnonce(nonce, nonce_count):
1541
1135
    raw = '%s:%d:%s:%s' % (nonce, nonce_count, time.ctime(),
1542
1136
                           urllib2.randombytes(8))
1543
 
    return osutils.sha_string(raw)[:16]
 
1137
    return sha.new(raw).hexdigest()[:16]
1544
1138
 
1545
1139
 
1546
1140
class DigestAuthHandler(AbstractAuthHandler):
1547
1141
    """A custom digest authentication handler."""
1548
1142
 
1549
 
    scheme = 'digest'
1550
 
    # Before basic as digest is a bit more secure and should be preferred
 
1143
    # Before basic as digest is a bit more secure
1551
1144
    handler_order = 490
1552
1145
 
1553
1146
    def auth_params_reusable(self, auth):
1557
1150
        return auth.get('scheme', None) == 'digest'
1558
1151
 
1559
1152
    def auth_match(self, header, auth):
1560
 
        scheme, raw_auth = self._parse_auth_header(header)
1561
 
        if scheme != self.scheme:
 
1153
        scheme, raw_auth = header.split(None, 1)
 
1154
        scheme = scheme.lower()
 
1155
        if scheme != 'digest':
1562
1156
            return False
1563
1157
 
1564
1158
        # Put the requested authentication info into a dict
1577
1171
        # Put useful info into auth
1578
1172
        self.update_auth(auth, 'scheme', scheme)
1579
1173
        self.update_auth(auth, 'realm', realm)
1580
 
        if auth.get('user', None) is None or auth.get('password', None) is None:
 
1174
        if auth['user'] is None or auth['password'] is None:
1581
1175
            user, password = self.get_user_password(auth)
1582
1176
            self.update_auth(auth, 'user', user)
1583
1177
            self.update_auth(auth, 'password', password)
1659
1253
    def build_password_prompt(self, auth):
1660
1254
        return self._build_password_prompt(auth)
1661
1255
 
1662
 
    def build_username_prompt(self, auth):
1663
 
        return self._build_username_prompt(auth)
1664
 
 
1665
1256
    def http_error_401(self, req, fp, code, msg, headers):
1666
1257
        return self.auth_required(req, headers)
1667
1258
 
1690
1281
 
1691
1282
    def build_password_prompt(self, auth):
1692
1283
        prompt = self._build_password_prompt(auth)
1693
 
        prompt = u'Proxy ' + prompt
1694
 
        return prompt
1695
 
 
1696
 
    def build_username_prompt(self, auth):
1697
 
        prompt = self._build_username_prompt(auth)
1698
 
        prompt = u'Proxy ' + prompt
 
1284
        prompt = 'Proxy ' + prompt
1699
1285
        return prompt
1700
1286
 
1701
1287
    def http_error_407(self, req, fp, code, msg, headers):
1718
1304
    """Custom proxy basic authentication handler"""
1719
1305
 
1720
1306
 
1721
 
class HTTPNegotiateAuthHandler(NegotiateAuthHandler, HTTPAuthHandler):
1722
 
    """Custom http negotiate authentication handler"""
1723
 
 
1724
 
 
1725
 
class ProxyNegotiateAuthHandler(NegotiateAuthHandler, ProxyAuthHandler):
1726
 
    """Custom proxy negotiate authentication handler"""
1727
 
 
1728
 
 
1729
1307
class HTTPErrorProcessor(urllib2.HTTPErrorProcessor):
1730
1308
    """Process HTTP error responses.
1731
1309
 
1782
1360
    def __init__(self,
1783
1361
                 connection=ConnectionHandler,
1784
1362
                 redirect=HTTPRedirectHandler,
1785
 
                 error=HTTPErrorProcessor,
1786
 
                 report_activity=None,
1787
 
                 ca_certs=None):
1788
 
        self._opener = urllib2.build_opener(
1789
 
            connection(report_activity=report_activity, ca_certs=ca_certs),
1790
 
            redirect, error,
 
1363
                 error=HTTPErrorProcessor,):
 
1364
        self._opener = urllib2.build_opener( \
 
1365
            connection, redirect, error,
1791
1366
            ProxyHandler(),
1792
1367
            HTTPBasicAuthHandler(),
1793
1368
            HTTPDigestAuthHandler(),
1794
 
            HTTPNegotiateAuthHandler(),
1795
1369
            ProxyBasicAuthHandler(),
1796
1370
            ProxyDigestAuthHandler(),
1797
 
            ProxyNegotiateAuthHandler(),
1798
1371
            HTTPHandler,
1799
1372
            HTTPSHandler,
1800
1373
            HTTPDefaultErrorHandler,