##// END OF EJS Templates
safehasattr: pass attribute name as string instead of bytes...
marmoute -
r51507:ecaf0008 default
parent child Browse files
Show More
@@ -1,661 +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, '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(
330 self.sock = socket.create_connection(
331 (self.host, self.port), self.timeout
331 (self.host, self.port), self.timeout
332 )
332 )
333
333
334 host = self.host
334 host = self.host
335 realhostport = self.realhostport # pytype: disable=attribute-error
335 realhostport = self.realhostport # pytype: disable=attribute-error
336 if realhostport: # use CONNECT proxy
336 if realhostport: # use CONNECT proxy
337 _generic_proxytunnel(self)
337 _generic_proxytunnel(self)
338 host = realhostport.rsplit(b':', 1)[0]
338 host = realhostport.rsplit(b':', 1)[0]
339 self.sock = sslutil.wrapsocket(
339 self.sock = sslutil.wrapsocket(
340 self.sock,
340 self.sock,
341 self.key_file,
341 self.key_file,
342 self.cert_file,
342 self.cert_file,
343 ui=self.ui, # pytype: disable=attribute-error
343 ui=self.ui, # pytype: disable=attribute-error
344 serverhostname=host,
344 serverhostname=host,
345 )
345 )
346 sslutil.validatesocket(self.sock)
346 sslutil.validatesocket(self.sock)
347
347
348 class httpshandler(keepalive.KeepAliveHandler, urlreq.httpshandler):
348 class httpshandler(keepalive.KeepAliveHandler, urlreq.httpshandler):
349 def __init__(self, ui, timeout=None):
349 def __init__(self, ui, timeout=None):
350 keepalive.KeepAliveHandler.__init__(self, timeout=timeout)
350 keepalive.KeepAliveHandler.__init__(self, timeout=timeout)
351 urlreq.httpshandler.__init__(self)
351 urlreq.httpshandler.__init__(self)
352 self.ui = ui
352 self.ui = ui
353 self.pwmgr = passwordmgr(self.ui, self.ui.httppasswordmgrdb)
353 self.pwmgr = passwordmgr(self.ui, self.ui.httppasswordmgrdb)
354
354
355 def _start_transaction(self, h, req):
355 def _start_transaction(self, h, req):
356 _generic_start_transaction(self, h, req)
356 _generic_start_transaction(self, h, req)
357 return keepalive.KeepAliveHandler._start_transaction(self, h, req)
357 return keepalive.KeepAliveHandler._start_transaction(self, h, req)
358
358
359 def https_open(self, req):
359 def https_open(self, req):
360 # urllibcompat.getfullurl() does not contain credentials
360 # urllibcompat.getfullurl() does not contain credentials
361 # and we may need them to match the certificates.
361 # and we may need them to match the certificates.
362 url = urllibcompat.getfullurl(req)
362 url = urllibcompat.getfullurl(req)
363 user, password = self.pwmgr.find_stored_password(url)
363 user, password = self.pwmgr.find_stored_password(url)
364 res = httpconnectionmod.readauthforuri(self.ui, url, user)
364 res = httpconnectionmod.readauthforuri(self.ui, url, user)
365 if res:
365 if res:
366 group, auth = res
366 group, auth = res
367 self.auth = auth
367 self.auth = auth
368 self.ui.debug(b"using auth.%s.* for authentication\n" % group)
368 self.ui.debug(b"using auth.%s.* for authentication\n" % group)
369 else:
369 else:
370 self.auth = None
370 self.auth = None
371 return self.do_open(self._makeconnection, req)
371 return self.do_open(self._makeconnection, req)
372
372
373 def _makeconnection(self, host, port=None, *args, **kwargs):
373 def _makeconnection(self, host, port=None, *args, **kwargs):
374 keyfile = None
374 keyfile = None
375 certfile = None
375 certfile = None
376
376
377 if len(args) >= 1: # key_file
377 if len(args) >= 1: # key_file
378 keyfile = args[0]
378 keyfile = args[0]
379 if len(args) >= 2: # cert_file
379 if len(args) >= 2: # cert_file
380 certfile = args[1]
380 certfile = args[1]
381 args = args[2:]
381 args = args[2:]
382
382
383 # if the user has specified different key/cert files in
383 # if the user has specified different key/cert files in
384 # hgrc, we prefer these
384 # hgrc, we prefer these
385 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:
386 keyfile = self.auth[b'key']
386 keyfile = self.auth[b'key']
387 certfile = self.auth[b'cert']
387 certfile = self.auth[b'cert']
388
388
389 conn = httpsconnection(
389 conn = httpsconnection(
390 host, port, keyfile, certfile, *args, **kwargs
390 host, port, keyfile, certfile, *args, **kwargs
391 )
391 )
392 conn.ui = self.ui
392 conn.ui = self.ui
393 return conn
393 return conn
394
394
395
395
396 class httpdigestauthhandler(urlreq.httpdigestauthhandler):
396 class httpdigestauthhandler(urlreq.httpdigestauthhandler):
397 def __init__(self, *args, **kwargs):
397 def __init__(self, *args, **kwargs):
398 urlreq.httpdigestauthhandler.__init__(self, *args, **kwargs)
398 urlreq.httpdigestauthhandler.__init__(self, *args, **kwargs)
399 self.retried_req = None
399 self.retried_req = None
400
400
401 def reset_retry_count(self):
401 def reset_retry_count(self):
402 # 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
403 # forever. We disable reset_retry_count completely and reset in
403 # forever. We disable reset_retry_count completely and reset in
404 # http_error_auth_reqed instead.
404 # http_error_auth_reqed instead.
405 pass
405 pass
406
406
407 def http_error_auth_reqed(self, auth_header, host, req, headers):
407 def http_error_auth_reqed(self, auth_header, host, req, headers):
408 # Reset the retry counter once for each request.
408 # Reset the retry counter once for each request.
409 if req is not self.retried_req:
409 if req is not self.retried_req:
410 self.retried_req = req
410 self.retried_req = req
411 self.retried = 0
411 self.retried = 0
412 return urlreq.httpdigestauthhandler.http_error_auth_reqed(
412 return urlreq.httpdigestauthhandler.http_error_auth_reqed(
413 self, auth_header, host, req, headers
413 self, auth_header, host, req, headers
414 )
414 )
415
415
416
416
417 class httpbasicauthhandler(urlreq.httpbasicauthhandler):
417 class httpbasicauthhandler(urlreq.httpbasicauthhandler):
418 def __init__(self, *args, **kwargs):
418 def __init__(self, *args, **kwargs):
419 self.auth = None
419 self.auth = None
420 urlreq.httpbasicauthhandler.__init__(self, *args, **kwargs)
420 urlreq.httpbasicauthhandler.__init__(self, *args, **kwargs)
421 self.retried_req = None
421 self.retried_req = None
422
422
423 def http_request(self, request):
423 def http_request(self, request):
424 if self.auth:
424 if self.auth:
425 request.add_unredirected_header(self.auth_header, self.auth)
425 request.add_unredirected_header(self.auth_header, self.auth)
426
426
427 return request
427 return request
428
428
429 def https_request(self, request):
429 def https_request(self, request):
430 if self.auth:
430 if self.auth:
431 request.add_unredirected_header(self.auth_header, self.auth)
431 request.add_unredirected_header(self.auth_header, self.auth)
432
432
433 return request
433 return request
434
434
435 def reset_retry_count(self):
435 def reset_retry_count(self):
436 # 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
437 # forever. We disable reset_retry_count completely and reset in
437 # forever. We disable reset_retry_count completely and reset in
438 # http_error_auth_reqed instead.
438 # http_error_auth_reqed instead.
439 pass
439 pass
440
440
441 def http_error_auth_reqed(self, auth_header, host, req, headers):
441 def http_error_auth_reqed(self, auth_header, host, req, headers):
442 # Reset the retry counter once for each request.
442 # Reset the retry counter once for each request.
443 if req is not self.retried_req:
443 if req is not self.retried_req:
444 self.retried_req = req
444 self.retried_req = req
445 self.retried = 0
445 self.retried = 0
446 return urlreq.httpbasicauthhandler.http_error_auth_reqed(
446 return urlreq.httpbasicauthhandler.http_error_auth_reqed(
447 self, auth_header, host, req, headers
447 self, auth_header, host, req, headers
448 )
448 )
449
449
450 def retry_http_basic_auth(self, host, req, realm):
450 def retry_http_basic_auth(self, host, req, realm):
451 user, pw = self.passwd.find_user_password(
451 user, pw = self.passwd.find_user_password(
452 realm, urllibcompat.getfullurl(req)
452 realm, urllibcompat.getfullurl(req)
453 )
453 )
454 if pw is not None:
454 if pw is not None:
455 raw = b"%s:%s" % (pycompat.bytesurl(user), pycompat.bytesurl(pw))
455 raw = b"%s:%s" % (pycompat.bytesurl(user), pycompat.bytesurl(pw))
456 auth = 'Basic %s' % pycompat.strurl(base64.b64encode(raw).strip())
456 auth = 'Basic %s' % pycompat.strurl(base64.b64encode(raw).strip())
457 if req.get_header(self.auth_header, None) == auth:
457 if req.get_header(self.auth_header, None) == auth:
458 return None
458 return None
459 self.auth = auth
459 self.auth = auth
460 req.add_unredirected_header(self.auth_header, auth)
460 req.add_unredirected_header(self.auth_header, auth)
461 return self.parent.open(req)
461 return self.parent.open(req)
462 else:
462 else:
463 return None
463 return None
464
464
465
465
466 class cookiehandler(urlreq.basehandler):
466 class cookiehandler(urlreq.basehandler):
467 def __init__(self, ui):
467 def __init__(self, ui):
468 self.cookiejar = None
468 self.cookiejar = None
469
469
470 cookiefile = ui.config(b'auth', b'cookiefile')
470 cookiefile = ui.config(b'auth', b'cookiefile')
471 if not cookiefile:
471 if not cookiefile:
472 return
472 return
473
473
474 cookiefile = util.expandpath(cookiefile)
474 cookiefile = util.expandpath(cookiefile)
475 try:
475 try:
476 cookiejar = util.cookielib.MozillaCookieJar(
476 cookiejar = util.cookielib.MozillaCookieJar(
477 pycompat.fsdecode(cookiefile)
477 pycompat.fsdecode(cookiefile)
478 )
478 )
479 cookiejar.load()
479 cookiejar.load()
480 self.cookiejar = cookiejar
480 self.cookiejar = cookiejar
481 except util.cookielib.LoadError as e:
481 except util.cookielib.LoadError as e:
482 ui.warn(
482 ui.warn(
483 _(
483 _(
484 b'(error loading cookie file %s: %s; continuing without '
484 b'(error loading cookie file %s: %s; continuing without '
485 b'cookies)\n'
485 b'cookies)\n'
486 )
486 )
487 % (cookiefile, stringutil.forcebytestr(e))
487 % (cookiefile, stringutil.forcebytestr(e))
488 )
488 )
489
489
490 def http_request(self, request):
490 def http_request(self, request):
491 if self.cookiejar:
491 if self.cookiejar:
492 self.cookiejar.add_cookie_header(request)
492 self.cookiejar.add_cookie_header(request)
493
493
494 return request
494 return request
495
495
496 def https_request(self, request):
496 def https_request(self, request):
497 if self.cookiejar:
497 if self.cookiejar:
498 self.cookiejar.add_cookie_header(request)
498 self.cookiejar.add_cookie_header(request)
499
499
500 return request
500 return request
501
501
502
502
503 handlerfuncs = []
503 handlerfuncs = []
504
504
505
505
506 def opener(
506 def opener(
507 ui,
507 ui,
508 authinfo=None,
508 authinfo=None,
509 useragent=None,
509 useragent=None,
510 loggingfh=None,
510 loggingfh=None,
511 loggingname=b's',
511 loggingname=b's',
512 loggingopts=None,
512 loggingopts=None,
513 sendaccept=True,
513 sendaccept=True,
514 ):
514 ):
515 """
515 """
516 construct an opener suitable for urllib2
516 construct an opener suitable for urllib2
517 authinfo will be added to the password manager
517 authinfo will be added to the password manager
518
518
519 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
520 ``logging*`` arguments are specified.
520 ``logging*`` arguments are specified.
521
521
522 ``loggingfh`` denotes a file object to log events to.
522 ``loggingfh`` denotes a file object to log events to.
523 ``loggingname`` denotes the name of the to print when logging.
523 ``loggingname`` denotes the name of the to print when logging.
524 ``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
525 ``util.socketobserver`` instance.
525 ``util.socketobserver`` instance.
526
526
527 ``sendaccept`` allows controlling whether the ``Accept`` request header
527 ``sendaccept`` allows controlling whether the ``Accept`` request header
528 is sent. The header is sent by default.
528 is sent. The header is sent by default.
529 """
529 """
530 timeout = ui.configwith(float, b'http', b'timeout')
530 timeout = ui.configwith(float, b'http', b'timeout')
531 handlers = []
531 handlers = []
532
532
533 if loggingfh:
533 if loggingfh:
534 handlers.append(
534 handlers.append(
535 logginghttphandler(
535 logginghttphandler(
536 loggingfh, loggingname, loggingopts or {}, timeout=timeout
536 loggingfh, loggingname, loggingopts or {}, timeout=timeout
537 )
537 )
538 )
538 )
539 # 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
540 # an HTTPS URL, we'll likely fail due to unknown protocol.
540 # an HTTPS URL, we'll likely fail due to unknown protocol.
541
541
542 else:
542 else:
543 handlers.append(httphandler(timeout=timeout))
543 handlers.append(httphandler(timeout=timeout))
544 if has_https:
544 if has_https:
545 handlers.append(httpshandler(ui, timeout=timeout))
545 handlers.append(httpshandler(ui, timeout=timeout))
546
546
547 handlers.append(proxyhandler(ui))
547 handlers.append(proxyhandler(ui))
548
548
549 passmgr = passwordmgr(ui, ui.httppasswordmgrdb)
549 passmgr = passwordmgr(ui, ui.httppasswordmgrdb)
550 if authinfo is not None:
550 if authinfo is not None:
551 realm, uris, user, passwd = authinfo
551 realm, uris, user, passwd = authinfo
552 saveduser, savedpass = passmgr.find_stored_password(uris[0])
552 saveduser, savedpass = passmgr.find_stored_password(uris[0])
553 if user != saveduser or passwd:
553 if user != saveduser or passwd:
554 passmgr.add_password(realm, uris, user, passwd)
554 passmgr.add_password(realm, uris, user, passwd)
555 ui.debug(
555 ui.debug(
556 b'http auth: user %s, password %s\n'
556 b'http auth: user %s, password %s\n'
557 % (user, passwd and b'*' * len(passwd) or b'not set')
557 % (user, passwd and b'*' * len(passwd) or b'not set')
558 )
558 )
559
559
560 handlers.extend(
560 handlers.extend(
561 (httpbasicauthhandler(passmgr), httpdigestauthhandler(passmgr))
561 (httpbasicauthhandler(passmgr), httpdigestauthhandler(passmgr))
562 )
562 )
563 handlers.extend([h(ui, passmgr) for h in handlerfuncs])
563 handlers.extend([h(ui, passmgr) for h in handlerfuncs])
564 handlers.append(cookiehandler(ui))
564 handlers.append(cookiehandler(ui))
565 opener = urlreq.buildopener(*handlers)
565 opener = urlreq.buildopener(*handlers)
566
566
567 # keepalive.py's handlers will populate these attributes if they exist.
567 # keepalive.py's handlers will populate these attributes if they exist.
568 opener.requestscount = 0
568 opener.requestscount = 0
569 opener.sentbytescount = 0
569 opener.sentbytescount = 0
570 opener.receivedbytescount = 0
570 opener.receivedbytescount = 0
571
571
572 # 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.
573 # protocol detection or feature negotiation: there are other
573 # protocol detection or feature negotiation: there are other
574 # facilities for that.
574 # facilities for that.
575 #
575 #
576 # "mercurial/proto-1.0" was the original user agent string and
576 # "mercurial/proto-1.0" was the original user agent string and
577 # exists for backwards compatibility reasons.
577 # exists for backwards compatibility reasons.
578 #
578 #
579 # The "(Mercurial %s)" string contains the distribution
579 # The "(Mercurial %s)" string contains the distribution
580 # name and version. Other client implementations should choose their
580 # name and version. Other client implementations should choose their
581 # own distribution name. Since servers should not be using the user
581 # own distribution name. Since servers should not be using the user
582 # agent string for anything, clients should be able to define whatever
582 # agent string for anything, clients should be able to define whatever
583 # user agent they deem appropriate.
583 # user agent they deem appropriate.
584 #
584 #
585 # The custom user agent is for lfs, because unfortunately some servers
585 # The custom user agent is for lfs, because unfortunately some servers
586 # do look at this value.
586 # do look at this value.
587 if not useragent:
587 if not useragent:
588 agent = b'mercurial/proto-1.0 (Mercurial %s)' % util.version()
588 agent = b'mercurial/proto-1.0 (Mercurial %s)' % util.version()
589 opener.addheaders = [('User-agent', pycompat.sysstr(agent))]
589 opener.addheaders = [('User-agent', pycompat.sysstr(agent))]
590 else:
590 else:
591 opener.addheaders = [('User-agent', pycompat.sysstr(useragent))]
591 opener.addheaders = [('User-agent', pycompat.sysstr(useragent))]
592
592
593 # 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
594 # 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
595 # compatibility reasons. Modern versions of the wire protocol use
595 # compatibility reasons. Modern versions of the wire protocol use
596 # X-HgProto-<N> for advertising client support.
596 # X-HgProto-<N> for advertising client support.
597 if sendaccept:
597 if sendaccept:
598 opener.addheaders.append(('Accept', 'application/mercurial-0.1'))
598 opener.addheaders.append(('Accept', 'application/mercurial-0.1'))
599
599
600 return opener
600 return opener
601
601
602
602
603 def open(ui, url_, data=None, sendaccept=True):
603 def open(ui, url_, data=None, sendaccept=True):
604 u = urlutil.url(url_)
604 u = urlutil.url(url_)
605 if u.scheme:
605 if u.scheme:
606 u.scheme = u.scheme.lower()
606 u.scheme = u.scheme.lower()
607 url_, authinfo = u.authinfo()
607 url_, authinfo = u.authinfo()
608 else:
608 else:
609 path = util.normpath(util.abspath(url_))
609 path = util.normpath(util.abspath(url_))
610 url_ = b'file://' + pycompat.bytesurl(
610 url_ = b'file://' + pycompat.bytesurl(
611 urlreq.pathname2url(pycompat.fsdecode(path))
611 urlreq.pathname2url(pycompat.fsdecode(path))
612 )
612 )
613 authinfo = None
613 authinfo = None
614 return opener(ui, authinfo, sendaccept=sendaccept).open(
614 return opener(ui, authinfo, sendaccept=sendaccept).open(
615 pycompat.strurl(url_), data
615 pycompat.strurl(url_), data
616 )
616 )
617
617
618
618
619 def wrapresponse(resp):
619 def wrapresponse(resp):
620 """Wrap a response object with common error handlers.
620 """Wrap a response object with common error handlers.
621
621
622 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
623 error and messaging.
623 error and messaging.
624 """
624 """
625 origread = resp.read
625 origread = resp.read
626
626
627 class readerproxy(resp.__class__):
627 class readerproxy(resp.__class__):
628 def read(self, size=None):
628 def read(self, size=None):
629 try:
629 try:
630 return origread(size)
630 return origread(size)
631 except httplib.IncompleteRead as e:
631 except httplib.IncompleteRead as e:
632 # e.expected is an integer if length known or None otherwise.
632 # e.expected is an integer if length known or None otherwise.
633 if e.expected:
633 if e.expected:
634 got = len(e.partial)
634 got = len(e.partial)
635 total = e.expected + got
635 total = e.expected + got
636 msg = _(
636 msg = _(
637 b'HTTP request error (incomplete response; '
637 b'HTTP request error (incomplete response; '
638 b'expected %d bytes got %d)'
638 b'expected %d bytes got %d)'
639 ) % (total, got)
639 ) % (total, got)
640 else:
640 else:
641 msg = _(b'HTTP request error (incomplete response)')
641 msg = _(b'HTTP request error (incomplete response)')
642
642
643 raise error.PeerTransportError(
643 raise error.PeerTransportError(
644 msg,
644 msg,
645 hint=_(
645 hint=_(
646 b'this may be an intermittent network failure; '
646 b'this may be an intermittent network failure; '
647 b'if the error persists, consider contacting the '
647 b'if the error persists, consider contacting the '
648 b'network or server operator'
648 b'network or server operator'
649 ),
649 ),
650 )
650 )
651 except httplib.HTTPException as e:
651 except httplib.HTTPException as e:
652 raise error.PeerTransportError(
652 raise error.PeerTransportError(
653 _(b'HTTP request error (%s)') % e,
653 _(b'HTTP request error (%s)') % e,
654 hint=_(
654 hint=_(
655 b'this may be an intermittent network failure; '
655 b'this may be an intermittent network failure; '
656 b'if the error persists, consider contacting the '
656 b'if the error persists, consider contacting the '
657 b'network or server operator'
657 b'network or server operator'
658 ),
658 ),
659 )
659 )
660
660
661 resp.__class__ = readerproxy
661 resp.__class__ = readerproxy
General Comments 0
You need to be logged in to leave comments. Login now