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