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