##// END OF EJS Templates
typing: suppress a few attribute errors in url.py...
Matt Harbison -
r50285:9f3edb30 default
parent child Browse files
Show More
@@ -1,651 +1,659
1 1 # url.py - HTTP handling for mercurial
2 2 #
3 3 # Copyright 2005, 2006, 2007, 2008 Olivia Mackall <olivia@selenic.com>
4 4 # Copyright 2006, 2007 Alexis S. L. Carvalho <alexis@cecm.usp.br>
5 5 # Copyright 2006 Vadim Gelfer <vadim.gelfer@gmail.com>
6 6 #
7 7 # This software may be used and distributed according to the terms of the
8 8 # GNU General Public License version 2 or any later version.
9 9
10 10
11 11 import base64
12 12 import socket
13 13
14 14 from .i18n import _
15 15 from . import (
16 16 encoding,
17 17 error,
18 18 httpconnection as httpconnectionmod,
19 19 keepalive,
20 20 pycompat,
21 21 sslutil,
22 22 urllibcompat,
23 23 util,
24 24 )
25 25 from .utils import (
26 26 stringutil,
27 27 urlutil,
28 28 )
29 29
30 30 httplib = util.httplib
31 31 stringio = util.stringio
32 32 urlerr = util.urlerr
33 33 urlreq = util.urlreq
34 34
35 35
36 36 def escape(s, quote=None):
37 37 """Replace special characters "&", "<" and ">" to HTML-safe sequences.
38 38 If the optional flag quote is true, the quotation mark character (")
39 39 is also translated.
40 40
41 41 This is the same as cgi.escape in Python, but always operates on
42 42 bytes, whereas cgi.escape in Python 3 only works on unicodes.
43 43 """
44 44 s = s.replace(b"&", b"&amp;")
45 45 s = s.replace(b"<", b"&lt;")
46 46 s = s.replace(b">", b"&gt;")
47 47 if quote:
48 48 s = s.replace(b'"', b"&quot;")
49 49 return s
50 50
51 51
52 52 class passwordmgr:
53 53 def __init__(self, ui, passwddb):
54 54 self.ui = ui
55 55 self.passwddb = passwddb
56 56
57 57 def add_password(self, realm, uri, user, passwd):
58 58 return self.passwddb.add_password(realm, uri, user, passwd)
59 59
60 60 def find_user_password(self, realm, authuri):
61 61 assert isinstance(realm, (type(None), str))
62 62 assert isinstance(authuri, str)
63 63 authinfo = self.passwddb.find_user_password(realm, authuri)
64 64 user, passwd = authinfo
65 65 user, passwd = pycompat.bytesurl(user), pycompat.bytesurl(passwd)
66 66 if user and passwd:
67 67 self._writedebug(user, passwd)
68 68 return (pycompat.strurl(user), pycompat.strurl(passwd))
69 69
70 70 if not user or not passwd:
71 71 res = httpconnectionmod.readauthforuri(self.ui, authuri, user)
72 72 if res:
73 73 group, auth = res
74 74 user, passwd = auth.get(b'username'), auth.get(b'password')
75 75 self.ui.debug(b"using auth.%s.* for authentication\n" % group)
76 76 if not user or not passwd:
77 77 u = urlutil.url(pycompat.bytesurl(authuri))
78 78 u.query = None
79 79 if not self.ui.interactive():
80 80 raise error.Abort(
81 81 _(b'http authorization required for %s')
82 82 % urlutil.hidepassword(bytes(u))
83 83 )
84 84
85 85 self.ui.write(
86 86 _(b"http authorization required for %s\n")
87 87 % urlutil.hidepassword(bytes(u))
88 88 )
89 89 self.ui.write(_(b"realm: %s\n") % pycompat.bytesurl(realm))
90 90 if user:
91 91 self.ui.write(_(b"user: %s\n") % user)
92 92 else:
93 93 user = self.ui.prompt(_(b"user:"), default=None)
94 94
95 95 if not passwd:
96 96 passwd = self.ui.getpass()
97 97
98 98 # As of Python 3.8, the default implementation of
99 99 # AbstractBasicAuthHandler.retry_http_basic_auth() assumes the user
100 100 # is set if pw is not None. This means (None, str) is not a valid
101 101 # return type of find_user_password().
102 102 if user is None:
103 103 return None, None
104 104
105 105 self.passwddb.add_password(realm, authuri, user, passwd)
106 106 self._writedebug(user, passwd)
107 107 return (pycompat.strurl(user), pycompat.strurl(passwd))
108 108
109 109 def _writedebug(self, user, passwd):
110 110 msg = _(b'http auth: user %s, password %s\n')
111 111 self.ui.debug(msg % (user, passwd and b'*' * len(passwd) or b'not set'))
112 112
113 113 def find_stored_password(self, authuri):
114 114 return self.passwddb.find_user_password(None, authuri)
115 115
116 116
117 117 class proxyhandler(urlreq.proxyhandler):
118 118 def __init__(self, ui):
119 119 proxyurl = ui.config(b"http_proxy", b"host") or encoding.environ.get(
120 120 b'http_proxy'
121 121 )
122 122 # XXX proxyauthinfo = None
123 123
124 124 if proxyurl:
125 125 # proxy can be proper url or host[:port]
126 126 if not (
127 127 proxyurl.startswith(b'http:') or proxyurl.startswith(b'https:')
128 128 ):
129 129 proxyurl = b'http://' + proxyurl + b'/'
130 130 proxy = urlutil.url(proxyurl)
131 131 if not proxy.user:
132 132 proxy.user = ui.config(b"http_proxy", b"user")
133 133 proxy.passwd = ui.config(b"http_proxy", b"passwd")
134 134
135 135 # see if we should use a proxy for this url
136 136 no_list = [b"localhost", b"127.0.0.1"]
137 137 no_list.extend(
138 138 [p.lower() for p in ui.configlist(b"http_proxy", b"no")]
139 139 )
140 140 no_list.extend(
141 141 [
142 142 p.strip().lower()
143 143 for p in encoding.environ.get(b"no_proxy", b'').split(b',')
144 144 if p.strip()
145 145 ]
146 146 )
147 147 # "http_proxy.always" config is for running tests on localhost
148 148 if ui.configbool(b"http_proxy", b"always"):
149 149 self.no_list = []
150 150 else:
151 151 self.no_list = no_list
152 152
153 153 # Keys and values need to be str because the standard library
154 154 # expects them to be.
155 155 proxyurl = str(proxy)
156 156 proxies = {'http': proxyurl, 'https': proxyurl}
157 157 ui.debug(
158 158 b'proxying through %s\n' % urlutil.hidepassword(bytes(proxy))
159 159 )
160 160 else:
161 161 proxies = {}
162 162
163 163 urlreq.proxyhandler.__init__(self, proxies)
164 164 self.ui = ui
165 165
166 166 def proxy_open(self, req, proxy, type_):
167 167 host = pycompat.bytesurl(urllibcompat.gethost(req)).split(b':')[0]
168 168 for e in self.no_list:
169 169 if host == e:
170 170 return None
171 171 if e.startswith(b'*.') and host.endswith(e[2:]):
172 172 return None
173 173 if e.startswith(b'.') and host.endswith(e[1:]):
174 174 return None
175 175
176 176 return urlreq.proxyhandler.proxy_open(self, req, proxy, type_)
177 177
178 178
179 179 def _gen_sendfile(orgsend):
180 180 def _sendfile(self, data):
181 181 # send a file
182 182 if isinstance(data, httpconnectionmod.httpsendfile):
183 183 # if auth required, some data sent twice, so rewind here
184 184 data.seek(0)
185 185 for chunk in util.filechunkiter(data):
186 186 orgsend(self, chunk)
187 187 else:
188 188 orgsend(self, data)
189 189
190 190 return _sendfile
191 191
192 192
193 193 has_https = util.safehasattr(urlreq, b'httpshandler')
194 194
195 195
196 196 class httpconnection(keepalive.HTTPConnection):
197 197 # must be able to send big bundle as stream.
198 198 send = _gen_sendfile(keepalive.HTTPConnection.send)
199 199
200 200
201 201 # Large parts of this function have their origin from before Python 2.6
202 202 # and could potentially be removed.
203 203 def _generic_start_transaction(handler, h, req):
204 204 tunnel_host = req._tunnel_host
205 205 if tunnel_host:
206 206 if tunnel_host[:7] not in ['http://', 'https:/']:
207 207 tunnel_host = 'https://' + tunnel_host
208 208 new_tunnel = True
209 209 else:
210 210 tunnel_host = urllibcompat.getselector(req)
211 211 new_tunnel = False
212 212
213 213 if new_tunnel or tunnel_host == urllibcompat.getfullurl(req): # has proxy
214 214 u = urlutil.url(pycompat.bytesurl(tunnel_host))
215 215 if new_tunnel or u.scheme == b'https': # only use CONNECT for HTTPS
216 216 h.realhostport = b':'.join([u.host, (u.port or b'443')])
217 217 h.headers = req.headers.copy()
218 218 h.headers.update(handler.parent.addheaders)
219 219 return
220 220
221 221 h.realhostport = None
222 222 h.headers = None
223 223
224 224
225 def _generic_proxytunnel(self):
225 def _generic_proxytunnel(self: "httpsconnection"):
226 headers = self.headers # pytype: disable=attribute-error
226 227 proxyheaders = {
227 pycompat.bytestr(x): pycompat.bytestr(self.headers[x])
228 for x in self.headers
228 pycompat.bytestr(x): pycompat.bytestr(headers[x])
229 for x in headers
229 230 if x.lower().startswith('proxy-')
230 231 }
231 self.send(b'CONNECT %s HTTP/1.0\r\n' % self.realhostport)
232 realhostport = self.realhostport # pytype: disable=attribute-error
233 self.send(b'CONNECT %s HTTP/1.0\r\n' % realhostport)
234
232 235 for header in proxyheaders.items():
233 236 self.send(b'%s: %s\r\n' % header)
234 237 self.send(b'\r\n')
235 238
236 239 # majority of the following code is duplicated from
237 240 # httplib.HTTPConnection as there are no adequate places to
238 241 # override functions to provide the needed functionality.
239 242
243 # pytype: disable=attribute-error
240 244 res = self.response_class(self.sock, method=self._method)
245 # pytype: enable=attribute-error
241 246
242 247 while True:
248 # pytype: disable=attribute-error
243 249 version, status, reason = res._read_status()
250 # pytype: enable=attribute-error
244 251 if status != httplib.CONTINUE:
245 252 break
246 253 # skip lines that are all whitespace
247 254 list(iter(lambda: res.fp.readline().strip(), b''))
248 255
249 256 if status == 200:
250 257 # skip lines until we find a blank line
251 258 list(iter(res.fp.readline, b'\r\n'))
252 259 else:
253 260 self.close()
254 261 raise socket.error(
255 262 "Tunnel connection failed: %d %s" % (status, reason.strip())
256 263 )
257 264
258 265
259 266 class httphandler(keepalive.HTTPHandler):
260 267 def http_open(self, req):
261 268 return self.do_open(httpconnection, req)
262 269
263 270 def _start_transaction(self, h, req):
264 271 _generic_start_transaction(self, h, req)
265 272 return keepalive.HTTPHandler._start_transaction(self, h, req)
266 273
267 274
268 275 class logginghttpconnection(keepalive.HTTPConnection):
269 276 def __init__(self, createconn, *args, **kwargs):
270 277 keepalive.HTTPConnection.__init__(self, *args, **kwargs)
271 278 self._create_connection = createconn
272 279
273 280
274 281 class logginghttphandler(httphandler):
275 282 """HTTP handler that logs socket I/O."""
276 283
277 284 def __init__(self, logfh, name, observeropts, timeout=None):
278 285 super(logginghttphandler, self).__init__(timeout=timeout)
279 286
280 287 self._logfh = logfh
281 288 self._logname = name
282 289 self._observeropts = observeropts
283 290
284 291 # do_open() calls the passed class to instantiate an HTTPConnection. We
285 292 # pass in a callable method that creates a custom HTTPConnection instance
286 293 # whose callback to create the socket knows how to proxy the socket.
287 294 def http_open(self, req):
288 295 return self.do_open(self._makeconnection, req)
289 296
290 297 def _makeconnection(self, *args, **kwargs):
291 298 def createconnection(*args, **kwargs):
292 299 sock = socket.create_connection(*args, **kwargs)
293 300 return util.makeloggingsocket(
294 301 self._logfh, sock, self._logname, **self._observeropts
295 302 )
296 303
297 304 return logginghttpconnection(createconnection, *args, **kwargs)
298 305
299 306
300 307 if has_https:
301 308
302 309 class httpsconnection(keepalive.HTTPConnection):
303 310 response_class = keepalive.HTTPResponse
304 311 default_port = httplib.HTTPS_PORT
305 312 # must be able to send big bundle as stream.
306 313 send = _gen_sendfile(keepalive.safesend)
307 314 getresponse = keepalive.wrapgetresponse(httplib.HTTPConnection)
308 315
309 316 def __init__(
310 317 self,
311 318 host,
312 319 port=None,
313 320 key_file=None,
314 321 cert_file=None,
315 322 *args,
316 323 **kwargs
317 324 ):
318 325 keepalive.HTTPConnection.__init__(self, host, port, *args, **kwargs)
319 326 self.key_file = key_file
320 327 self.cert_file = cert_file
321 328
322 329 def connect(self):
323 330 self.sock = socket.create_connection((self.host, self.port))
324 331
325 332 host = self.host
326 if self.realhostport: # use CONNECT proxy
333 realhostport = self.realhostport # pytype: disable=attribute-error
334 if realhostport: # use CONNECT proxy
327 335 _generic_proxytunnel(self)
328 host = self.realhostport.rsplit(b':', 1)[0]
336 host = realhostport.rsplit(b':', 1)[0]
329 337 self.sock = sslutil.wrapsocket(
330 338 self.sock,
331 339 self.key_file,
332 340 self.cert_file,
333 ui=self.ui,
341 ui=self.ui, # pytype: disable=attribute-error
334 342 serverhostname=host,
335 343 )
336 344 sslutil.validatesocket(self.sock)
337 345
338 346 class httpshandler(keepalive.KeepAliveHandler, urlreq.httpshandler):
339 347 def __init__(self, ui, timeout=None):
340 348 keepalive.KeepAliveHandler.__init__(self, timeout=timeout)
341 349 urlreq.httpshandler.__init__(self)
342 350 self.ui = ui
343 351 self.pwmgr = passwordmgr(self.ui, self.ui.httppasswordmgrdb)
344 352
345 353 def _start_transaction(self, h, req):
346 354 _generic_start_transaction(self, h, req)
347 355 return keepalive.KeepAliveHandler._start_transaction(self, h, req)
348 356
349 357 def https_open(self, req):
350 358 # urllibcompat.getfullurl() does not contain credentials
351 359 # and we may need them to match the certificates.
352 360 url = urllibcompat.getfullurl(req)
353 361 user, password = self.pwmgr.find_stored_password(url)
354 362 res = httpconnectionmod.readauthforuri(self.ui, url, user)
355 363 if res:
356 364 group, auth = res
357 365 self.auth = auth
358 366 self.ui.debug(b"using auth.%s.* for authentication\n" % group)
359 367 else:
360 368 self.auth = None
361 369 return self.do_open(self._makeconnection, req)
362 370
363 371 def _makeconnection(self, host, port=None, *args, **kwargs):
364 372 keyfile = None
365 373 certfile = None
366 374
367 375 if len(args) >= 1: # key_file
368 376 keyfile = args[0]
369 377 if len(args) >= 2: # cert_file
370 378 certfile = args[1]
371 379 args = args[2:]
372 380
373 381 # if the user has specified different key/cert files in
374 382 # hgrc, we prefer these
375 383 if self.auth and b'key' in self.auth and b'cert' in self.auth:
376 384 keyfile = self.auth[b'key']
377 385 certfile = self.auth[b'cert']
378 386
379 387 conn = httpsconnection(
380 388 host, port, keyfile, certfile, *args, **kwargs
381 389 )
382 390 conn.ui = self.ui
383 391 return conn
384 392
385 393
386 394 class httpdigestauthhandler(urlreq.httpdigestauthhandler):
387 395 def __init__(self, *args, **kwargs):
388 396 urlreq.httpdigestauthhandler.__init__(self, *args, **kwargs)
389 397 self.retried_req = None
390 398
391 399 def reset_retry_count(self):
392 400 # Python 2.6.5 will call this on 401 or 407 errors and thus loop
393 401 # forever. We disable reset_retry_count completely and reset in
394 402 # http_error_auth_reqed instead.
395 403 pass
396 404
397 405 def http_error_auth_reqed(self, auth_header, host, req, headers):
398 406 # Reset the retry counter once for each request.
399 407 if req is not self.retried_req:
400 408 self.retried_req = req
401 409 self.retried = 0
402 410 return urlreq.httpdigestauthhandler.http_error_auth_reqed(
403 411 self, auth_header, host, req, headers
404 412 )
405 413
406 414
407 415 class httpbasicauthhandler(urlreq.httpbasicauthhandler):
408 416 def __init__(self, *args, **kwargs):
409 417 self.auth = None
410 418 urlreq.httpbasicauthhandler.__init__(self, *args, **kwargs)
411 419 self.retried_req = None
412 420
413 421 def http_request(self, request):
414 422 if self.auth:
415 423 request.add_unredirected_header(self.auth_header, self.auth)
416 424
417 425 return request
418 426
419 427 def https_request(self, request):
420 428 if self.auth:
421 429 request.add_unredirected_header(self.auth_header, self.auth)
422 430
423 431 return request
424 432
425 433 def reset_retry_count(self):
426 434 # Python 2.6.5 will call this on 401 or 407 errors and thus loop
427 435 # forever. We disable reset_retry_count completely and reset in
428 436 # http_error_auth_reqed instead.
429 437 pass
430 438
431 439 def http_error_auth_reqed(self, auth_header, host, req, headers):
432 440 # Reset the retry counter once for each request.
433 441 if req is not self.retried_req:
434 442 self.retried_req = req
435 443 self.retried = 0
436 444 return urlreq.httpbasicauthhandler.http_error_auth_reqed(
437 445 self, auth_header, host, req, headers
438 446 )
439 447
440 448 def retry_http_basic_auth(self, host, req, realm):
441 449 user, pw = self.passwd.find_user_password(
442 450 realm, urllibcompat.getfullurl(req)
443 451 )
444 452 if pw is not None:
445 453 raw = b"%s:%s" % (pycompat.bytesurl(user), pycompat.bytesurl(pw))
446 454 auth = 'Basic %s' % pycompat.strurl(base64.b64encode(raw).strip())
447 455 if req.get_header(self.auth_header, None) == auth:
448 456 return None
449 457 self.auth = auth
450 458 req.add_unredirected_header(self.auth_header, auth)
451 459 return self.parent.open(req)
452 460 else:
453 461 return None
454 462
455 463
456 464 class cookiehandler(urlreq.basehandler):
457 465 def __init__(self, ui):
458 466 self.cookiejar = None
459 467
460 468 cookiefile = ui.config(b'auth', b'cookiefile')
461 469 if not cookiefile:
462 470 return
463 471
464 472 cookiefile = util.expandpath(cookiefile)
465 473 try:
466 474 cookiejar = util.cookielib.MozillaCookieJar(
467 475 pycompat.fsdecode(cookiefile)
468 476 )
469 477 cookiejar.load()
470 478 self.cookiejar = cookiejar
471 479 except util.cookielib.LoadError as e:
472 480 ui.warn(
473 481 _(
474 482 b'(error loading cookie file %s: %s; continuing without '
475 483 b'cookies)\n'
476 484 )
477 485 % (cookiefile, stringutil.forcebytestr(e))
478 486 )
479 487
480 488 def http_request(self, request):
481 489 if self.cookiejar:
482 490 self.cookiejar.add_cookie_header(request)
483 491
484 492 return request
485 493
486 494 def https_request(self, request):
487 495 if self.cookiejar:
488 496 self.cookiejar.add_cookie_header(request)
489 497
490 498 return request
491 499
492 500
493 501 handlerfuncs = []
494 502
495 503
496 504 def opener(
497 505 ui,
498 506 authinfo=None,
499 507 useragent=None,
500 508 loggingfh=None,
501 509 loggingname=b's',
502 510 loggingopts=None,
503 511 sendaccept=True,
504 512 ):
505 513 """
506 514 construct an opener suitable for urllib2
507 515 authinfo will be added to the password manager
508 516
509 517 The opener can be configured to log socket events if the various
510 518 ``logging*`` arguments are specified.
511 519
512 520 ``loggingfh`` denotes a file object to log events to.
513 521 ``loggingname`` denotes the name of the to print when logging.
514 522 ``loggingopts`` is a dict of keyword arguments to pass to the constructed
515 523 ``util.socketobserver`` instance.
516 524
517 525 ``sendaccept`` allows controlling whether the ``Accept`` request header
518 526 is sent. The header is sent by default.
519 527 """
520 528 timeout = ui.configwith(float, b'http', b'timeout')
521 529 handlers = []
522 530
523 531 if loggingfh:
524 532 handlers.append(
525 533 logginghttphandler(
526 534 loggingfh, loggingname, loggingopts or {}, timeout=timeout
527 535 )
528 536 )
529 537 # We don't yet support HTTPS when logging I/O. If we attempt to open
530 538 # an HTTPS URL, we'll likely fail due to unknown protocol.
531 539
532 540 else:
533 541 handlers.append(httphandler(timeout=timeout))
534 542 if has_https:
535 543 handlers.append(httpshandler(ui, timeout=timeout))
536 544
537 545 handlers.append(proxyhandler(ui))
538 546
539 547 passmgr = passwordmgr(ui, ui.httppasswordmgrdb)
540 548 if authinfo is not None:
541 549 realm, uris, user, passwd = authinfo
542 550 saveduser, savedpass = passmgr.find_stored_password(uris[0])
543 551 if user != saveduser or passwd:
544 552 passmgr.add_password(realm, uris, user, passwd)
545 553 ui.debug(
546 554 b'http auth: user %s, password %s\n'
547 555 % (user, passwd and b'*' * len(passwd) or b'not set')
548 556 )
549 557
550 558 handlers.extend(
551 559 (httpbasicauthhandler(passmgr), httpdigestauthhandler(passmgr))
552 560 )
553 561 handlers.extend([h(ui, passmgr) for h in handlerfuncs])
554 562 handlers.append(cookiehandler(ui))
555 563 opener = urlreq.buildopener(*handlers)
556 564
557 565 # keepalive.py's handlers will populate these attributes if they exist.
558 566 opener.requestscount = 0
559 567 opener.sentbytescount = 0
560 568 opener.receivedbytescount = 0
561 569
562 570 # The user agent should should *NOT* be used by servers for e.g.
563 571 # protocol detection or feature negotiation: there are other
564 572 # facilities for that.
565 573 #
566 574 # "mercurial/proto-1.0" was the original user agent string and
567 575 # exists for backwards compatibility reasons.
568 576 #
569 577 # The "(Mercurial %s)" string contains the distribution
570 578 # name and version. Other client implementations should choose their
571 579 # own distribution name. Since servers should not be using the user
572 580 # agent string for anything, clients should be able to define whatever
573 581 # user agent they deem appropriate.
574 582 #
575 583 # The custom user agent is for lfs, because unfortunately some servers
576 584 # do look at this value.
577 585 if not useragent:
578 586 agent = b'mercurial/proto-1.0 (Mercurial %s)' % util.version()
579 587 opener.addheaders = [('User-agent', pycompat.sysstr(agent))]
580 588 else:
581 589 opener.addheaders = [('User-agent', pycompat.sysstr(useragent))]
582 590
583 591 # This header should only be needed by wire protocol requests. But it has
584 592 # been sent on all requests since forever. We keep sending it for backwards
585 593 # compatibility reasons. Modern versions of the wire protocol use
586 594 # X-HgProto-<N> for advertising client support.
587 595 if sendaccept:
588 596 opener.addheaders.append(('Accept', 'application/mercurial-0.1'))
589 597
590 598 return opener
591 599
592 600
593 601 def open(ui, url_, data=None, sendaccept=True):
594 602 u = urlutil.url(url_)
595 603 if u.scheme:
596 604 u.scheme = u.scheme.lower()
597 605 url_, authinfo = u.authinfo()
598 606 else:
599 607 path = util.normpath(util.abspath(url_))
600 608 url_ = b'file://' + pycompat.bytesurl(
601 609 urlreq.pathname2url(pycompat.fsdecode(path))
602 610 )
603 611 authinfo = None
604 612 return opener(ui, authinfo, sendaccept=sendaccept).open(
605 613 pycompat.strurl(url_), data
606 614 )
607 615
608 616
609 617 def wrapresponse(resp):
610 618 """Wrap a response object with common error handlers.
611 619
612 620 This ensures that any I/O from any consumer raises the appropriate
613 621 error and messaging.
614 622 """
615 623 origread = resp.read
616 624
617 625 class readerproxy(resp.__class__):
618 626 def read(self, size=None):
619 627 try:
620 628 return origread(size)
621 629 except httplib.IncompleteRead as e:
622 630 # e.expected is an integer if length known or None otherwise.
623 631 if e.expected:
624 632 got = len(e.partial)
625 633 total = e.expected + got
626 634 msg = _(
627 635 b'HTTP request error (incomplete response; '
628 636 b'expected %d bytes got %d)'
629 637 ) % (total, got)
630 638 else:
631 639 msg = _(b'HTTP request error (incomplete response)')
632 640
633 641 raise error.PeerTransportError(
634 642 msg,
635 643 hint=_(
636 644 b'this may be an intermittent network failure; '
637 645 b'if the error persists, consider contacting the '
638 646 b'network or server operator'
639 647 ),
640 648 )
641 649 except httplib.HTTPException as e:
642 650 raise error.PeerTransportError(
643 651 _(b'HTTP request error (%s)') % e,
644 652 hint=_(
645 653 b'this may be an intermittent network failure; '
646 654 b'if the error persists, consider contacting the '
647 655 b'network or server operator'
648 656 ),
649 657 )
650 658
651 659 resp.__class__ = readerproxy
General Comments 0
You need to be logged in to leave comments. Login now