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