/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 bzrlib/transport/http/_urllib2_wrappers.py

  • Committer: John Arbash Meinel
  • Date: 2007-07-11 23:45:20 UTC
  • mfrom: (2601 +trunk)
  • mto: This revision was merged to the branch mainline in revision 2643.
  • Revision ID: john@arbash-meinel.com-20070711234520-do3h7zw8skbathpz
[merge] bzr.dev 2601

Show diffs side-by-side

added added

removed removed

Lines of Context:
108
108
                if self.debuglevel > 0:
109
109
                    print "Consumed body: [%s]" % body
110
110
            self.close()
 
111
        elif self.status == 200:
 
112
            # Whatever the request is, it went ok, so we surely don't want to
 
113
            # close the connection. Some cases are not correctly detected by
 
114
            # httplib.HTTPConnection.getresponse (called by
 
115
            # httplib.HTTPResponse.begin). The CONNECT response for the https
 
116
            # through proxy case is one.
 
117
            self.will_close = False
111
118
 
112
119
 
113
120
# Not inheriting from 'object' because httplib.HTTPConnection doesn't.
125
132
        # Preserve our preciousss
126
133
        sock = self.sock
127
134
        self.sock = None
 
135
        # Let httplib.HTTPConnection do its housekeeping 
128
136
        self.close()
 
137
        # Restore our preciousss
129
138
        self.sock = sock
130
139
 
131
140
 
132
141
class HTTPConnection(AbstractHTTPConnection, httplib.HTTPConnection):
133
 
    pass
 
142
 
 
143
    # XXX: Needs refactoring at the caller level.
 
144
    def __init__(self, host, port=None, strict=None, proxied_host=None):
 
145
        httplib.HTTPConnection.__init__(self, host, port, strict)
 
146
        self.proxied_host = proxied_host
134
147
 
135
148
 
136
149
class HTTPSConnection(AbstractHTTPConnection, httplib.HTTPSConnection):
137
 
    pass
 
150
 
 
151
    def __init__(self, host, port=None, key_file=None, cert_file=None,
 
152
                 strict=None, proxied_host=None):
 
153
        httplib.HTTPSConnection.__init__(self, host, port,
 
154
                                         key_file, cert_file, strict)
 
155
        self.proxied_host = proxied_host
 
156
 
 
157
    def connect(self):
 
158
        httplib.HTTPConnection.connect(self)
 
159
        if self.proxied_host is None:
 
160
            self.connect_to_origin()
 
161
 
 
162
    def connect_to_origin(self):
 
163
        ssl = socket.ssl(self.sock, self.key_file, self.cert_file)
 
164
        self.sock = httplib.FakeSocket(self.sock, ssl)
138
165
 
139
166
 
140
167
class Request(urllib2.Request):
153
180
 
154
181
    def __init__(self, method, url, data=None, headers={},
155
182
                 origin_req_host=None, unverifiable=False,
156
 
                 connection=None, parent=None,):
 
183
                 connection=None, parent=None,
 
184
                 accepted_errors=None):
157
185
        urllib2.Request.__init__(self, url, data, headers,
158
186
                                 origin_req_host, unverifiable)
159
187
        self.method = method
160
188
        self.connection = connection
 
189
        self.accepted_errors = accepted_errors
161
190
        # To handle redirections
162
191
        self.parent = parent
163
192
        self.redirected_to = None
169
198
        # Some authentication schemes may add more entries.
170
199
        self.auth = {}
171
200
        self.proxy_auth = {}
 
201
        self.proxied_host = None
172
202
 
173
203
    def get_method(self):
174
204
        return self.method
175
205
 
 
206
    def set_proxy(self, proxy, type):
 
207
        """Set the proxy and remember the proxied host."""
 
208
        self.proxied_host = self.get_host()
 
209
        urllib2.Request.set_proxy(self, proxy, type)
 
210
 
 
211
 
 
212
class _ConnectRequest(Request):
 
213
 
 
214
    def __init__(self, request):
 
215
        """Constructor
 
216
        
 
217
        :param request: the first request sent to the proxied host, already
 
218
            processed by the opener (i.e. proxied_host is already set).
 
219
        """
 
220
        # We give a fake url and redefine get_selector or urllib2 will be
 
221
        # confused
 
222
        Request.__init__(self, 'CONNECT', request.get_full_url(),
 
223
                         connection=request.connection)
 
224
        assert request.proxied_host is not None
 
225
        self.proxied_host = request.proxied_host
 
226
 
 
227
    def get_selector(self):
 
228
        return self.proxied_host
 
229
 
 
230
    def set_proxy(self, proxy, type):
 
231
        """Set the proxy without remembering the proxied host.
 
232
 
 
233
        We already know the proxied host by definition, the CONNECT request
 
234
        occurs only when the connection goes through a proxy. The usual
 
235
        processing (masquerade the request so that the connection is done to
 
236
        the proxy while the request is targeted at another host) does not apply
 
237
        here. In fact, the connection is already established with proxy and we
 
238
        just want to enable the SSL tunneling.
 
239
        """
 
240
        urllib2.Request.set_proxy(self, proxy, type)
 
241
 
176
242
 
177
243
def extract_credentials(url):
178
244
    """Extracts credentials information from url.
213
279
    return '%s://%s' % (scheme, host)
214
280
 
215
281
 
216
 
# The urlib2.xxxAuthHandler handle the authentication of the
217
 
# requests, to do that, they need an urllib2 PasswordManager *at
218
 
# build time*. We also need one to reuse the passwords entered by
219
 
# the user.
 
282
# The AuthHandler classes handle the authentication of the requests, to do
 
283
# that, they need a PasswordManager *at build time*. We also need one to reuse
 
284
# the passwords entered by the user.
220
285
class PasswordManager(urllib2.HTTPPasswordMgrWithDefaultRealm):
221
286
 
222
287
    def __init__(self):
242
307
            # handled in the higher levels
243
308
            raise errors.InvalidURL(request.get_full_url(), 'no host given.')
244
309
 
245
 
        # We create a connection (but it will not connect yet)
 
310
        # We create a connection (but it will not connect until the first
 
311
        # request is made)
246
312
        try:
247
 
            connection = http_connection_class(host)
 
313
            connection = http_connection_class(
 
314
                host, proxied_host=request.proxied_host)
248
315
        except httplib.InvalidURL, exception:
249
316
            # There is only one occurrence of InvalidURL in httplib
250
317
            raise errors.InvalidURL(request.get_full_url(),
367
434
                else:
368
435
                    # All other exception are considered connection related.
369
436
 
370
 
                    # httplib.HTTPException should indicate a bug
371
 
                    # in the urllib implementation, somewhow the
372
 
                    # httplib pipeline is in an incorrect state,
373
 
                    # we retry in hope that this will correct the
374
 
                    # problem but that may need investigation
375
 
                    # (note that no such bug is known as of
 
437
                    # httplib.HTTPException should indicate a bug in our
 
438
                    # urllib-based implementation, somewhow the httplib
 
439
                    # pipeline is in an incorrect state, we retry in hope that
 
440
                    # this will correct the problem but that may need
 
441
                    # investigation (note that no such bug is known as of
376
442
                    # 20061005 --vila).
377
443
 
378
444
                    # socket errors generally occurs for reasons
383
449
                    # FIXME: and then there is HTTPError raised by:
384
450
                    # - HTTPDefaultErrorHandler (we define our own)
385
451
                    # - HTTPRedirectHandler.redirect_request 
386
 
                    # - AbstractDigestAuthHandler.http_error_auth_reqed
387
452
 
388
453
                    my_exception = errors.ConnectionError(
389
454
                        msg= 'while sending %s %s:' % (request.get_method(),
498
563
class HTTPSHandler(AbstractHTTPHandler):
499
564
    """A custom handler that just thunks into HTTPSConnection"""
500
565
 
 
566
    https_request = AbstractHTTPHandler.http_request
 
567
 
501
568
    def https_open(self, request):
 
569
        connection = request.connection
 
570
        assert isinstance(connection, HTTPSConnection)
 
571
        if connection.sock is None and \
 
572
                connection.proxied_host is not None and \
 
573
                request.get_method() != 'CONNECT' : # Don't loop
 
574
            # FIXME: We need a gazillion connection tests here, but we still
 
575
            # miss a https server :-( :
 
576
            # - with and without proxy
 
577
            # - with and without certificate
 
578
            # - with self-signed certificate
 
579
            # - with and without authentication
 
580
            # - with good and bad credentials (especially the proxy auth aound
 
581
            #   CONNECT)
 
582
            # - with basic and digest schemes
 
583
            # - reconnection on errors
 
584
            # - connection persistence behaviour (including reconnection)
 
585
 
 
586
            # We are about to connect for the first time via a proxy, we must
 
587
            # issue a CONNECT request first to establish the encrypted link
 
588
            connect = _ConnectRequest(request)
 
589
            response = self.parent.open(connect)
 
590
            if response.code != 200:
 
591
                raise ConnectionError("Can't connect to %s via proxy %s" % (
 
592
                        connect.proxied_host, self.host))
 
593
            # Housekeeping
 
594
            connection.fake_close()
 
595
            # Establish the connection encryption 
 
596
            connection.connect_to_origin()
 
597
            # Propagate the connection to the original request
 
598
            request.connection = connection
502
599
        return self.do_open(HTTPSConnection, request)
503
600
 
504
 
 
505
601
class HTTPRedirectHandler(urllib2.HTTPRedirectHandler):
506
602
    """Handles redirect requests.
507
603
 
623
719
class ProxyHandler(urllib2.ProxyHandler):
624
720
    """Handles proxy setting.
625
721
 
626
 
    Copied and modified from urllib2 to be able to modify the
627
 
    request during the request pre-processing instead of
628
 
    modifying it at _open time. As we capture (or create) the
629
 
    connection object during request processing, _open time was
630
 
    too late.
631
 
 
632
 
    Note that the proxy handling *may* modify the protocol used;
633
 
    the request may be against an https server proxied through an
634
 
    http proxy. So, https_request will be called, but later it's
635
 
    really http_open that will be called. This explain why we
636
 
    don't have to call self.parent.open as the urllib2 did.
 
722
    Copied and modified from urllib2 to be able to modify the request during
 
723
    the request pre-processing instead of modifying it at _open time. As we
 
724
    capture (or create) the connection object during request processing, _open
 
725
    time was too late.
 
726
 
 
727
    The main task is to modify the request so that the connection is done to
 
728
    the proxy while the request still refers to the destination host.
 
729
 
 
730
    Note: the proxy handling *may* modify the protocol used; the request may be
 
731
    against an https server proxied through an http proxy. So, https_request
 
732
    will be called, but later it's really http_open that will be called. This
 
733
    explains why we don't have to call self.parent.open as the urllib2 did.
637
734
    """
638
735
 
639
736
    # Proxies must be in front
713
810
        proxy = self.get_proxy_env_var(type)
714
811
        if self._debuglevel > 0:
715
812
            print 'set_proxy %s_request for %r' % (type, proxy)
 
813
        # FIXME: python 2.5 urlparse provides a better _parse_proxy which can
 
814
        # grok user:password@host:port as well as
 
815
        # http://user:password@host:port
 
816
 
716
817
        # Extract credentials from the url and store them in the
717
818
        # password manager so that the proxy AuthHandler can use
718
819
        # them later.
781
882
      successful and the request authentication parameters have been updated.
782
883
    """
783
884
 
 
885
    _max_retry = 2
 
886
    """We don't want to retry authenticating endlessly"""
 
887
 
784
888
    # The following attributes should be defined by daughter
785
889
    # classes:
786
890
    # - auth_required_header:  the header received from the server
790
894
        self.password_manager = password_manager
791
895
        self.find_user_password = password_manager.find_user_password
792
896
        self.add_password = password_manager.add_password
 
897
        self._retry_count = None
793
898
 
794
899
    def update_auth(self, auth, key, value):
795
900
        """Update a value in auth marking the auth as modified if needed"""
805
910
        :param headers: The headers for the authentication error response.
806
911
        :return: None or the response for the authenticated request.
807
912
        """
 
913
        # Don't try  to authenticate endlessly
 
914
        if self._retry_count is None:
 
915
            # The retry being recusrsive calls, None identify the first try
 
916
            self._retry_count = 0
 
917
        else:
 
918
            self._retry_count += 1
 
919
            if self._retry_count > self._max_retry:
 
920
                # Let's be ready for next round
 
921
                self._retry_count = None
 
922
                return None
808
923
        server_header = headers.get(self.auth_required_header, None)
809
924
        if server_header is None:
810
925
            # The http error MUST have the associated
880
995
        :param request: The succesfully authenticated request.
881
996
        :param response: The server response (may contain auth info).
882
997
        """
883
 
        pass
 
998
        # It may happen that we need to reconnect later, let's be ready
 
999
        self._retry_count = None
884
1000
 
885
1001
    def get_password(self, user, authuri, realm=None):
886
1002
        """Ask user for a password if none is already available."""
916
1032
class BasicAuthHandler(AbstractAuthHandler):
917
1033
    """A custom basic authentication handler."""
918
1034
 
 
1035
    handler_order = 500
 
1036
 
919
1037
    auth_regexp = re.compile('realm="([^"]*)"', re.I)
920
1038
 
921
1039
    def build_auth_header(self, auth, request):
972
1090
class DigestAuthHandler(AbstractAuthHandler):
973
1091
    """A custom digest authentication handler."""
974
1092
 
 
1093
    # Before basic as digest is a bit more secure
 
1094
    handler_order = 490
 
1095
 
975
1096
    def auth_params_reusable(self, auth):
976
1097
        # If the auth scheme is known, it means a previous
977
1098
        # authentication was successful, all information is
1133
1254
    instead, we leave our Transport handle them.
1134
1255
    """
1135
1256
 
 
1257
    accepted_errors = [200, # Ok
 
1258
                       206, # Partial content
 
1259
                       404, # Not found
 
1260
                       ]
 
1261
    """The error codes the caller will handle.
 
1262
 
 
1263
    This can be specialized in the request on a case-by case basis, but the
 
1264
    common cases are covered here.
 
1265
    """
 
1266
 
1136
1267
    def http_response(self, request, response):
1137
1268
        code, msg, hdrs = response.code, response.msg, response.info()
1138
1269
 
1139
 
        if code not in (200, # Ok
1140
 
                        206, # Partial content
1141
 
                        404, # Not found
1142
 
                        ):
 
1270
        accepted_errors = request.accepted_errors
 
1271
        if accepted_errors is None:
 
1272
            accepted_errors = self.accepted_errors
 
1273
 
 
1274
        if code not in accepted_errors:
1143
1275
            response = self.parent.error('http', request, response,
1144
1276
                                         code, msg, hdrs)
1145
1277
        return response
1151
1283
    """Translate common errors into bzr Exceptions"""
1152
1284
 
1153
1285
    def http_error_default(self, req, fp, code, msg, hdrs):
1154
 
        if code == 404:
1155
 
            raise errors.NoSuchFile(req.get_selector(),
1156
 
                                    extra=HTTPError(req.get_full_url(),
1157
 
                                                    code, msg,
1158
 
                                                    hdrs, fp))
1159
 
        elif code == 403:
 
1286
        if code == 403:
1160
1287
            raise errors.TransportError('Server refuses to fullfil the request')
1161
1288
        elif code == 416:
1162
1289
            # We don't know which, but one of the ranges we
1168
1295
                                             'Unable to handle http code %d: %s'
1169
1296
                                             % (code, msg))
1170
1297
 
 
1298
 
1171
1299
class Opener(object):
1172
1300
    """A wrapper around urllib2.build_opener
1173
1301
 
1191
1319
            HTTPSHandler,
1192
1320
            HTTPDefaultErrorHandler,
1193
1321
            )
 
1322
 
1194
1323
        self.open = self._opener.open
1195
1324
        if DEBUG >= 2:
1196
1325
            # When dealing with handler order, it's easy to mess