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