##// END OF EJS Templates
url: don't ignore timeout for https connections...
Julien Cristau -
r51244:3bb7c56e stable
parent child Browse files
Show More
@@ -1,659 +1,661 b''
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 225 def _generic_proxytunnel(self: "httpsconnection"):
226 226 headers = self.headers # pytype: disable=attribute-error
227 227 proxyheaders = {
228 228 pycompat.bytestr(x): pycompat.bytestr(headers[x])
229 229 for x in headers
230 230 if x.lower().startswith('proxy-')
231 231 }
232 232 realhostport = self.realhostport # pytype: disable=attribute-error
233 233 self.send(b'CONNECT %s HTTP/1.0\r\n' % realhostport)
234 234
235 235 for header in proxyheaders.items():
236 236 self.send(b'%s: %s\r\n' % header)
237 237 self.send(b'\r\n')
238 238
239 239 # majority of the following code is duplicated from
240 240 # httplib.HTTPConnection as there are no adequate places to
241 241 # override functions to provide the needed functionality.
242 242
243 243 # pytype: disable=attribute-error
244 244 res = self.response_class(self.sock, method=self._method)
245 245 # pytype: enable=attribute-error
246 246
247 247 while True:
248 248 # pytype: disable=attribute-error
249 249 version, status, reason = res._read_status()
250 250 # pytype: enable=attribute-error
251 251 if status != httplib.CONTINUE:
252 252 break
253 253 # skip lines that are all whitespace
254 254 list(iter(lambda: res.fp.readline().strip(), b''))
255 255
256 256 if status == 200:
257 257 # skip lines until we find a blank line
258 258 list(iter(res.fp.readline, b'\r\n'))
259 259 else:
260 260 self.close()
261 261 raise socket.error(
262 262 "Tunnel connection failed: %d %s" % (status, reason.strip())
263 263 )
264 264
265 265
266 266 class httphandler(keepalive.HTTPHandler):
267 267 def http_open(self, req):
268 268 return self.do_open(httpconnection, req)
269 269
270 270 def _start_transaction(self, h, req):
271 271 _generic_start_transaction(self, h, req)
272 272 return keepalive.HTTPHandler._start_transaction(self, h, req)
273 273
274 274
275 275 class logginghttpconnection(keepalive.HTTPConnection):
276 276 def __init__(self, createconn, *args, **kwargs):
277 277 keepalive.HTTPConnection.__init__(self, *args, **kwargs)
278 278 self._create_connection = createconn
279 279
280 280
281 281 class logginghttphandler(httphandler):
282 282 """HTTP handler that logs socket I/O."""
283 283
284 284 def __init__(self, logfh, name, observeropts, timeout=None):
285 285 super(logginghttphandler, self).__init__(timeout=timeout)
286 286
287 287 self._logfh = logfh
288 288 self._logname = name
289 289 self._observeropts = observeropts
290 290
291 291 # do_open() calls the passed class to instantiate an HTTPConnection. We
292 292 # pass in a callable method that creates a custom HTTPConnection instance
293 293 # whose callback to create the socket knows how to proxy the socket.
294 294 def http_open(self, req):
295 295 return self.do_open(self._makeconnection, req)
296 296
297 297 def _makeconnection(self, *args, **kwargs):
298 298 def createconnection(*args, **kwargs):
299 299 sock = socket.create_connection(*args, **kwargs)
300 300 return util.makeloggingsocket(
301 301 self._logfh, sock, self._logname, **self._observeropts
302 302 )
303 303
304 304 return logginghttpconnection(createconnection, *args, **kwargs)
305 305
306 306
307 307 if has_https:
308 308
309 309 class httpsconnection(keepalive.HTTPConnection):
310 310 response_class = keepalive.HTTPResponse
311 311 default_port = httplib.HTTPS_PORT
312 312 # must be able to send big bundle as stream.
313 313 send = _gen_sendfile(keepalive.safesend)
314 314 getresponse = keepalive.wrapgetresponse(httplib.HTTPConnection)
315 315
316 316 def __init__(
317 317 self,
318 318 host,
319 319 port=None,
320 320 key_file=None,
321 321 cert_file=None,
322 322 *args,
323 323 **kwargs
324 324 ):
325 325 keepalive.HTTPConnection.__init__(self, host, port, *args, **kwargs)
326 326 self.key_file = key_file
327 327 self.cert_file = cert_file
328 328
329 329 def connect(self):
330 self.sock = socket.create_connection((self.host, self.port))
330 self.sock = socket.create_connection(
331 (self.host, self.port), self.timeout
332 )
331 333
332 334 host = self.host
333 335 realhostport = self.realhostport # pytype: disable=attribute-error
334 336 if realhostport: # use CONNECT proxy
335 337 _generic_proxytunnel(self)
336 338 host = realhostport.rsplit(b':', 1)[0]
337 339 self.sock = sslutil.wrapsocket(
338 340 self.sock,
339 341 self.key_file,
340 342 self.cert_file,
341 343 ui=self.ui, # pytype: disable=attribute-error
342 344 serverhostname=host,
343 345 )
344 346 sslutil.validatesocket(self.sock)
345 347
346 348 class httpshandler(keepalive.KeepAliveHandler, urlreq.httpshandler):
347 349 def __init__(self, ui, timeout=None):
348 350 keepalive.KeepAliveHandler.__init__(self, timeout=timeout)
349 351 urlreq.httpshandler.__init__(self)
350 352 self.ui = ui
351 353 self.pwmgr = passwordmgr(self.ui, self.ui.httppasswordmgrdb)
352 354
353 355 def _start_transaction(self, h, req):
354 356 _generic_start_transaction(self, h, req)
355 357 return keepalive.KeepAliveHandler._start_transaction(self, h, req)
356 358
357 359 def https_open(self, req):
358 360 # urllibcompat.getfullurl() does not contain credentials
359 361 # and we may need them to match the certificates.
360 362 url = urllibcompat.getfullurl(req)
361 363 user, password = self.pwmgr.find_stored_password(url)
362 364 res = httpconnectionmod.readauthforuri(self.ui, url, user)
363 365 if res:
364 366 group, auth = res
365 367 self.auth = auth
366 368 self.ui.debug(b"using auth.%s.* for authentication\n" % group)
367 369 else:
368 370 self.auth = None
369 371 return self.do_open(self._makeconnection, req)
370 372
371 373 def _makeconnection(self, host, port=None, *args, **kwargs):
372 374 keyfile = None
373 375 certfile = None
374 376
375 377 if len(args) >= 1: # key_file
376 378 keyfile = args[0]
377 379 if len(args) >= 2: # cert_file
378 380 certfile = args[1]
379 381 args = args[2:]
380 382
381 383 # if the user has specified different key/cert files in
382 384 # hgrc, we prefer these
383 385 if self.auth and b'key' in self.auth and b'cert' in self.auth:
384 386 keyfile = self.auth[b'key']
385 387 certfile = self.auth[b'cert']
386 388
387 389 conn = httpsconnection(
388 390 host, port, keyfile, certfile, *args, **kwargs
389 391 )
390 392 conn.ui = self.ui
391 393 return conn
392 394
393 395
394 396 class httpdigestauthhandler(urlreq.httpdigestauthhandler):
395 397 def __init__(self, *args, **kwargs):
396 398 urlreq.httpdigestauthhandler.__init__(self, *args, **kwargs)
397 399 self.retried_req = None
398 400
399 401 def reset_retry_count(self):
400 402 # Python 2.6.5 will call this on 401 or 407 errors and thus loop
401 403 # forever. We disable reset_retry_count completely and reset in
402 404 # http_error_auth_reqed instead.
403 405 pass
404 406
405 407 def http_error_auth_reqed(self, auth_header, host, req, headers):
406 408 # Reset the retry counter once for each request.
407 409 if req is not self.retried_req:
408 410 self.retried_req = req
409 411 self.retried = 0
410 412 return urlreq.httpdigestauthhandler.http_error_auth_reqed(
411 413 self, auth_header, host, req, headers
412 414 )
413 415
414 416
415 417 class httpbasicauthhandler(urlreq.httpbasicauthhandler):
416 418 def __init__(self, *args, **kwargs):
417 419 self.auth = None
418 420 urlreq.httpbasicauthhandler.__init__(self, *args, **kwargs)
419 421 self.retried_req = None
420 422
421 423 def http_request(self, request):
422 424 if self.auth:
423 425 request.add_unredirected_header(self.auth_header, self.auth)
424 426
425 427 return request
426 428
427 429 def https_request(self, request):
428 430 if self.auth:
429 431 request.add_unredirected_header(self.auth_header, self.auth)
430 432
431 433 return request
432 434
433 435 def reset_retry_count(self):
434 436 # Python 2.6.5 will call this on 401 or 407 errors and thus loop
435 437 # forever. We disable reset_retry_count completely and reset in
436 438 # http_error_auth_reqed instead.
437 439 pass
438 440
439 441 def http_error_auth_reqed(self, auth_header, host, req, headers):
440 442 # Reset the retry counter once for each request.
441 443 if req is not self.retried_req:
442 444 self.retried_req = req
443 445 self.retried = 0
444 446 return urlreq.httpbasicauthhandler.http_error_auth_reqed(
445 447 self, auth_header, host, req, headers
446 448 )
447 449
448 450 def retry_http_basic_auth(self, host, req, realm):
449 451 user, pw = self.passwd.find_user_password(
450 452 realm, urllibcompat.getfullurl(req)
451 453 )
452 454 if pw is not None:
453 455 raw = b"%s:%s" % (pycompat.bytesurl(user), pycompat.bytesurl(pw))
454 456 auth = 'Basic %s' % pycompat.strurl(base64.b64encode(raw).strip())
455 457 if req.get_header(self.auth_header, None) == auth:
456 458 return None
457 459 self.auth = auth
458 460 req.add_unredirected_header(self.auth_header, auth)
459 461 return self.parent.open(req)
460 462 else:
461 463 return None
462 464
463 465
464 466 class cookiehandler(urlreq.basehandler):
465 467 def __init__(self, ui):
466 468 self.cookiejar = None
467 469
468 470 cookiefile = ui.config(b'auth', b'cookiefile')
469 471 if not cookiefile:
470 472 return
471 473
472 474 cookiefile = util.expandpath(cookiefile)
473 475 try:
474 476 cookiejar = util.cookielib.MozillaCookieJar(
475 477 pycompat.fsdecode(cookiefile)
476 478 )
477 479 cookiejar.load()
478 480 self.cookiejar = cookiejar
479 481 except util.cookielib.LoadError as e:
480 482 ui.warn(
481 483 _(
482 484 b'(error loading cookie file %s: %s; continuing without '
483 485 b'cookies)\n'
484 486 )
485 487 % (cookiefile, stringutil.forcebytestr(e))
486 488 )
487 489
488 490 def http_request(self, request):
489 491 if self.cookiejar:
490 492 self.cookiejar.add_cookie_header(request)
491 493
492 494 return request
493 495
494 496 def https_request(self, request):
495 497 if self.cookiejar:
496 498 self.cookiejar.add_cookie_header(request)
497 499
498 500 return request
499 501
500 502
501 503 handlerfuncs = []
502 504
503 505
504 506 def opener(
505 507 ui,
506 508 authinfo=None,
507 509 useragent=None,
508 510 loggingfh=None,
509 511 loggingname=b's',
510 512 loggingopts=None,
511 513 sendaccept=True,
512 514 ):
513 515 """
514 516 construct an opener suitable for urllib2
515 517 authinfo will be added to the password manager
516 518
517 519 The opener can be configured to log socket events if the various
518 520 ``logging*`` arguments are specified.
519 521
520 522 ``loggingfh`` denotes a file object to log events to.
521 523 ``loggingname`` denotes the name of the to print when logging.
522 524 ``loggingopts`` is a dict of keyword arguments to pass to the constructed
523 525 ``util.socketobserver`` instance.
524 526
525 527 ``sendaccept`` allows controlling whether the ``Accept`` request header
526 528 is sent. The header is sent by default.
527 529 """
528 530 timeout = ui.configwith(float, b'http', b'timeout')
529 531 handlers = []
530 532
531 533 if loggingfh:
532 534 handlers.append(
533 535 logginghttphandler(
534 536 loggingfh, loggingname, loggingopts or {}, timeout=timeout
535 537 )
536 538 )
537 539 # We don't yet support HTTPS when logging I/O. If we attempt to open
538 540 # an HTTPS URL, we'll likely fail due to unknown protocol.
539 541
540 542 else:
541 543 handlers.append(httphandler(timeout=timeout))
542 544 if has_https:
543 545 handlers.append(httpshandler(ui, timeout=timeout))
544 546
545 547 handlers.append(proxyhandler(ui))
546 548
547 549 passmgr = passwordmgr(ui, ui.httppasswordmgrdb)
548 550 if authinfo is not None:
549 551 realm, uris, user, passwd = authinfo
550 552 saveduser, savedpass = passmgr.find_stored_password(uris[0])
551 553 if user != saveduser or passwd:
552 554 passmgr.add_password(realm, uris, user, passwd)
553 555 ui.debug(
554 556 b'http auth: user %s, password %s\n'
555 557 % (user, passwd and b'*' * len(passwd) or b'not set')
556 558 )
557 559
558 560 handlers.extend(
559 561 (httpbasicauthhandler(passmgr), httpdigestauthhandler(passmgr))
560 562 )
561 563 handlers.extend([h(ui, passmgr) for h in handlerfuncs])
562 564 handlers.append(cookiehandler(ui))
563 565 opener = urlreq.buildopener(*handlers)
564 566
565 567 # keepalive.py's handlers will populate these attributes if they exist.
566 568 opener.requestscount = 0
567 569 opener.sentbytescount = 0
568 570 opener.receivedbytescount = 0
569 571
570 572 # The user agent should should *NOT* be used by servers for e.g.
571 573 # protocol detection or feature negotiation: there are other
572 574 # facilities for that.
573 575 #
574 576 # "mercurial/proto-1.0" was the original user agent string and
575 577 # exists for backwards compatibility reasons.
576 578 #
577 579 # The "(Mercurial %s)" string contains the distribution
578 580 # name and version. Other client implementations should choose their
579 581 # own distribution name. Since servers should not be using the user
580 582 # agent string for anything, clients should be able to define whatever
581 583 # user agent they deem appropriate.
582 584 #
583 585 # The custom user agent is for lfs, because unfortunately some servers
584 586 # do look at this value.
585 587 if not useragent:
586 588 agent = b'mercurial/proto-1.0 (Mercurial %s)' % util.version()
587 589 opener.addheaders = [('User-agent', pycompat.sysstr(agent))]
588 590 else:
589 591 opener.addheaders = [('User-agent', pycompat.sysstr(useragent))]
590 592
591 593 # This header should only be needed by wire protocol requests. But it has
592 594 # been sent on all requests since forever. We keep sending it for backwards
593 595 # compatibility reasons. Modern versions of the wire protocol use
594 596 # X-HgProto-<N> for advertising client support.
595 597 if sendaccept:
596 598 opener.addheaders.append(('Accept', 'application/mercurial-0.1'))
597 599
598 600 return opener
599 601
600 602
601 603 def open(ui, url_, data=None, sendaccept=True):
602 604 u = urlutil.url(url_)
603 605 if u.scheme:
604 606 u.scheme = u.scheme.lower()
605 607 url_, authinfo = u.authinfo()
606 608 else:
607 609 path = util.normpath(util.abspath(url_))
608 610 url_ = b'file://' + pycompat.bytesurl(
609 611 urlreq.pathname2url(pycompat.fsdecode(path))
610 612 )
611 613 authinfo = None
612 614 return opener(ui, authinfo, sendaccept=sendaccept).open(
613 615 pycompat.strurl(url_), data
614 616 )
615 617
616 618
617 619 def wrapresponse(resp):
618 620 """Wrap a response object with common error handlers.
619 621
620 622 This ensures that any I/O from any consumer raises the appropriate
621 623 error and messaging.
622 624 """
623 625 origread = resp.read
624 626
625 627 class readerproxy(resp.__class__):
626 628 def read(self, size=None):
627 629 try:
628 630 return origread(size)
629 631 except httplib.IncompleteRead as e:
630 632 # e.expected is an integer if length known or None otherwise.
631 633 if e.expected:
632 634 got = len(e.partial)
633 635 total = e.expected + got
634 636 msg = _(
635 637 b'HTTP request error (incomplete response; '
636 638 b'expected %d bytes got %d)'
637 639 ) % (total, got)
638 640 else:
639 641 msg = _(b'HTTP request error (incomplete response)')
640 642
641 643 raise error.PeerTransportError(
642 644 msg,
643 645 hint=_(
644 646 b'this may be an intermittent network failure; '
645 647 b'if the error persists, consider contacting the '
646 648 b'network or server operator'
647 649 ),
648 650 )
649 651 except httplib.HTTPException as e:
650 652 raise error.PeerTransportError(
651 653 _(b'HTTP request error (%s)') % e,
652 654 hint=_(
653 655 b'this may be an intermittent network failure; '
654 656 b'if the error persists, consider contacting the '
655 657 b'network or server operator'
656 658 ),
657 659 )
658 660
659 661 resp.__class__ = readerproxy
General Comments 0
You need to be logged in to leave comments. Login now