##// END OF EJS Templates
httpconnection: remove a mutable default argument...
Pierre-Yves David -
r26347:e9a35411 default
parent child Browse files
Show More
@@ -1,284 +1,286 b''
1 # httpconnection.py - urllib2 handler for new http support
1 # httpconnection.py - urllib2 handler for new http support
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 # Copyright 2011 Google, Inc.
6 # Copyright 2011 Google, Inc.
7 #
7 #
8 # This software may be used and distributed according to the terms of the
8 # This software may be used and distributed according to the terms of the
9 # GNU General Public License version 2 or any later version.
9 # GNU General Public License version 2 or any later version.
10 import logging
10 import logging
11 import socket
11 import socket
12 import urllib
12 import urllib
13 import urllib2
13 import urllib2
14 import os
14 import os
15
15
16 from mercurial import httpclient
16 from mercurial import httpclient
17 from mercurial import sslutil
17 from mercurial import sslutil
18 from mercurial import util
18 from mercurial import util
19 from mercurial.i18n import _
19 from mercurial.i18n import _
20
20
21 # moved here from url.py to avoid a cycle
21 # moved here from url.py to avoid a cycle
22 class httpsendfile(object):
22 class httpsendfile(object):
23 """This is a wrapper around the objects returned by python's "open".
23 """This is a wrapper around the objects returned by python's "open".
24
24
25 Its purpose is to send file-like objects via HTTP.
25 Its purpose is to send file-like objects via HTTP.
26 It do however not define a __len__ attribute because the length
26 It do however not define a __len__ attribute because the length
27 might be more than Py_ssize_t can handle.
27 might be more than Py_ssize_t can handle.
28 """
28 """
29
29
30 def __init__(self, ui, *args, **kwargs):
30 def __init__(self, ui, *args, **kwargs):
31 # We can't just "self._data = open(*args, **kwargs)" here because there
31 # We can't just "self._data = open(*args, **kwargs)" here because there
32 # is an "open" function defined in this module that shadows the global
32 # is an "open" function defined in this module that shadows the global
33 # one
33 # one
34 self.ui = ui
34 self.ui = ui
35 self._data = open(*args, **kwargs)
35 self._data = open(*args, **kwargs)
36 self.seek = self._data.seek
36 self.seek = self._data.seek
37 self.close = self._data.close
37 self.close = self._data.close
38 self.write = self._data.write
38 self.write = self._data.write
39 self.length = os.fstat(self._data.fileno()).st_size
39 self.length = os.fstat(self._data.fileno()).st_size
40 self._pos = 0
40 self._pos = 0
41 self._total = self.length // 1024 * 2
41 self._total = self.length // 1024 * 2
42
42
43 def read(self, *args, **kwargs):
43 def read(self, *args, **kwargs):
44 try:
44 try:
45 ret = self._data.read(*args, **kwargs)
45 ret = self._data.read(*args, **kwargs)
46 except EOFError:
46 except EOFError:
47 self.ui.progress(_('sending'), None)
47 self.ui.progress(_('sending'), None)
48 self._pos += len(ret)
48 self._pos += len(ret)
49 # We pass double the max for total because we currently have
49 # We pass double the max for total because we currently have
50 # to send the bundle twice in the case of a server that
50 # to send the bundle twice in the case of a server that
51 # requires authentication. Since we can't know until we try
51 # requires authentication. Since we can't know until we try
52 # once whether authentication will be required, just lie to
52 # once whether authentication will be required, just lie to
53 # the user and maybe the push succeeds suddenly at 50%.
53 # the user and maybe the push succeeds suddenly at 50%.
54 self.ui.progress(_('sending'), self._pos // 1024,
54 self.ui.progress(_('sending'), self._pos // 1024,
55 unit=_('kb'), total=self._total)
55 unit=_('kb'), total=self._total)
56 return ret
56 return ret
57
57
58 # moved here from url.py to avoid a cycle
58 # moved here from url.py to avoid a cycle
59 def readauthforuri(ui, uri, user):
59 def readauthforuri(ui, uri, user):
60 # Read configuration
60 # Read configuration
61 config = dict()
61 config = dict()
62 for key, val in ui.configitems('auth'):
62 for key, val in ui.configitems('auth'):
63 if '.' not in key:
63 if '.' not in key:
64 ui.warn(_("ignoring invalid [auth] key '%s'\n") % key)
64 ui.warn(_("ignoring invalid [auth] key '%s'\n") % key)
65 continue
65 continue
66 group, setting = key.rsplit('.', 1)
66 group, setting = key.rsplit('.', 1)
67 gdict = config.setdefault(group, dict())
67 gdict = config.setdefault(group, dict())
68 if setting in ('username', 'cert', 'key'):
68 if setting in ('username', 'cert', 'key'):
69 val = util.expandpath(val)
69 val = util.expandpath(val)
70 gdict[setting] = val
70 gdict[setting] = val
71
71
72 # Find the best match
72 # Find the best match
73 scheme, hostpath = uri.split('://', 1)
73 scheme, hostpath = uri.split('://', 1)
74 bestuser = None
74 bestuser = None
75 bestlen = 0
75 bestlen = 0
76 bestauth = None
76 bestauth = None
77 for group, auth in config.iteritems():
77 for group, auth in config.iteritems():
78 if user and user != auth.get('username', user):
78 if user and user != auth.get('username', user):
79 # If a username was set in the URI, the entry username
79 # If a username was set in the URI, the entry username
80 # must either match it or be unset
80 # must either match it or be unset
81 continue
81 continue
82 prefix = auth.get('prefix')
82 prefix = auth.get('prefix')
83 if not prefix:
83 if not prefix:
84 continue
84 continue
85 p = prefix.split('://', 1)
85 p = prefix.split('://', 1)
86 if len(p) > 1:
86 if len(p) > 1:
87 schemes, prefix = [p[0]], p[1]
87 schemes, prefix = [p[0]], p[1]
88 else:
88 else:
89 schemes = (auth.get('schemes') or 'https').split()
89 schemes = (auth.get('schemes') or 'https').split()
90 if (prefix == '*' or hostpath.startswith(prefix)) and \
90 if (prefix == '*' or hostpath.startswith(prefix)) and \
91 (len(prefix) > bestlen or (len(prefix) == bestlen and \
91 (len(prefix) > bestlen or (len(prefix) == bestlen and \
92 not bestuser and 'username' in auth)) \
92 not bestuser and 'username' in auth)) \
93 and scheme in schemes:
93 and scheme in schemes:
94 bestlen = len(prefix)
94 bestlen = len(prefix)
95 bestauth = group, auth
95 bestauth = group, auth
96 bestuser = auth.get('username')
96 bestuser = auth.get('username')
97 if user and not bestuser:
97 if user and not bestuser:
98 auth['username'] = user
98 auth['username'] = user
99 return bestauth
99 return bestauth
100
100
101 # Mercurial (at least until we can remove the old codepath) requires
101 # Mercurial (at least until we can remove the old codepath) requires
102 # that the http response object be sufficiently file-like, so we
102 # that the http response object be sufficiently file-like, so we
103 # provide a close() method here.
103 # provide a close() method here.
104 class HTTPResponse(httpclient.HTTPResponse):
104 class HTTPResponse(httpclient.HTTPResponse):
105 def close(self):
105 def close(self):
106 pass
106 pass
107
107
108 class HTTPConnection(httpclient.HTTPConnection):
108 class HTTPConnection(httpclient.HTTPConnection):
109 response_class = HTTPResponse
109 response_class = HTTPResponse
110 def request(self, method, uri, body=None, headers={}):
110 def request(self, method, uri, body=None, headers=None):
111 if headers is None:
112 headers = {}
111 if isinstance(body, httpsendfile):
113 if isinstance(body, httpsendfile):
112 body.seek(0)
114 body.seek(0)
113 httpclient.HTTPConnection.request(self, method, uri, body=body,
115 httpclient.HTTPConnection.request(self, method, uri, body=body,
114 headers=headers)
116 headers=headers)
115
117
116
118
117 _configuredlogging = False
119 _configuredlogging = False
118 LOGFMT = '%(levelname)s:%(name)s:%(lineno)d:%(message)s'
120 LOGFMT = '%(levelname)s:%(name)s:%(lineno)d:%(message)s'
119 # Subclass BOTH of these because otherwise urllib2 "helpfully"
121 # Subclass BOTH of these because otherwise urllib2 "helpfully"
120 # reinserts them since it notices we don't include any subclasses of
122 # reinserts them since it notices we don't include any subclasses of
121 # them.
123 # them.
122 class http2handler(urllib2.HTTPHandler, urllib2.HTTPSHandler):
124 class http2handler(urllib2.HTTPHandler, urllib2.HTTPSHandler):
123 def __init__(self, ui, pwmgr):
125 def __init__(self, ui, pwmgr):
124 global _configuredlogging
126 global _configuredlogging
125 urllib2.AbstractHTTPHandler.__init__(self)
127 urllib2.AbstractHTTPHandler.__init__(self)
126 self.ui = ui
128 self.ui = ui
127 self.pwmgr = pwmgr
129 self.pwmgr = pwmgr
128 self._connections = {}
130 self._connections = {}
129 # developer config: ui.http2debuglevel
131 # developer config: ui.http2debuglevel
130 loglevel = ui.config('ui', 'http2debuglevel', default=None)
132 loglevel = ui.config('ui', 'http2debuglevel', default=None)
131 if loglevel and not _configuredlogging:
133 if loglevel and not _configuredlogging:
132 _configuredlogging = True
134 _configuredlogging = True
133 logger = logging.getLogger('mercurial.httpclient')
135 logger = logging.getLogger('mercurial.httpclient')
134 logger.setLevel(getattr(logging, loglevel.upper()))
136 logger.setLevel(getattr(logging, loglevel.upper()))
135 handler = logging.StreamHandler()
137 handler = logging.StreamHandler()
136 handler.setFormatter(logging.Formatter(LOGFMT))
138 handler.setFormatter(logging.Formatter(LOGFMT))
137 logger.addHandler(handler)
139 logger.addHandler(handler)
138
140
139 def close_all(self):
141 def close_all(self):
140 """Close and remove all connection objects being kept for reuse."""
142 """Close and remove all connection objects being kept for reuse."""
141 for openconns in self._connections.values():
143 for openconns in self._connections.values():
142 for conn in openconns:
144 for conn in openconns:
143 conn.close()
145 conn.close()
144 self._connections = {}
146 self._connections = {}
145
147
146 # shamelessly borrowed from urllib2.AbstractHTTPHandler
148 # shamelessly borrowed from urllib2.AbstractHTTPHandler
147 def do_open(self, http_class, req, use_ssl):
149 def do_open(self, http_class, req, use_ssl):
148 """Return an addinfourl object for the request, using http_class.
150 """Return an addinfourl object for the request, using http_class.
149
151
150 http_class must implement the HTTPConnection API from httplib.
152 http_class must implement the HTTPConnection API from httplib.
151 The addinfourl return value is a file-like object. It also
153 The addinfourl return value is a file-like object. It also
152 has methods and attributes including:
154 has methods and attributes including:
153 - info(): return a mimetools.Message object for the headers
155 - info(): return a mimetools.Message object for the headers
154 - geturl(): return the original request URL
156 - geturl(): return the original request URL
155 - code: HTTP status code
157 - code: HTTP status code
156 """
158 """
157 # If using a proxy, the host returned by get_host() is
159 # If using a proxy, the host returned by get_host() is
158 # actually the proxy. On Python 2.6.1, the real destination
160 # actually the proxy. On Python 2.6.1, the real destination
159 # hostname is encoded in the URI in the urllib2 request
161 # hostname is encoded in the URI in the urllib2 request
160 # object. On Python 2.6.5, it's stored in the _tunnel_host
162 # object. On Python 2.6.5, it's stored in the _tunnel_host
161 # attribute which has no accessor.
163 # attribute which has no accessor.
162 tunhost = getattr(req, '_tunnel_host', None)
164 tunhost = getattr(req, '_tunnel_host', None)
163 host = req.get_host()
165 host = req.get_host()
164 if tunhost:
166 if tunhost:
165 proxyhost = host
167 proxyhost = host
166 host = tunhost
168 host = tunhost
167 elif req.has_proxy():
169 elif req.has_proxy():
168 proxyhost = req.get_host()
170 proxyhost = req.get_host()
169 host = req.get_selector().split('://', 1)[1].split('/', 1)[0]
171 host = req.get_selector().split('://', 1)[1].split('/', 1)[0]
170 else:
172 else:
171 proxyhost = None
173 proxyhost = None
172
174
173 if proxyhost:
175 if proxyhost:
174 if ':' in proxyhost:
176 if ':' in proxyhost:
175 # Note: this means we'll explode if we try and use an
177 # Note: this means we'll explode if we try and use an
176 # IPv6 http proxy. This isn't a regression, so we
178 # IPv6 http proxy. This isn't a regression, so we
177 # won't worry about it for now.
179 # won't worry about it for now.
178 proxyhost, proxyport = proxyhost.rsplit(':', 1)
180 proxyhost, proxyport = proxyhost.rsplit(':', 1)
179 else:
181 else:
180 proxyport = 3128 # squid default
182 proxyport = 3128 # squid default
181 proxy = (proxyhost, proxyport)
183 proxy = (proxyhost, proxyport)
182 else:
184 else:
183 proxy = None
185 proxy = None
184
186
185 if not host:
187 if not host:
186 raise urllib2.URLError('no host given')
188 raise urllib2.URLError('no host given')
187
189
188 connkey = use_ssl, host, proxy
190 connkey = use_ssl, host, proxy
189 allconns = self._connections.get(connkey, [])
191 allconns = self._connections.get(connkey, [])
190 conns = [c for c in allconns if not c.busy()]
192 conns = [c for c in allconns if not c.busy()]
191 if conns:
193 if conns:
192 h = conns[0]
194 h = conns[0]
193 else:
195 else:
194 if allconns:
196 if allconns:
195 self.ui.debug('all connections for %s busy, making a new '
197 self.ui.debug('all connections for %s busy, making a new '
196 'one\n' % host)
198 'one\n' % host)
197 timeout = None
199 timeout = None
198 if req.timeout is not socket._GLOBAL_DEFAULT_TIMEOUT:
200 if req.timeout is not socket._GLOBAL_DEFAULT_TIMEOUT:
199 timeout = req.timeout
201 timeout = req.timeout
200 h = http_class(host, timeout=timeout, proxy_hostport=proxy)
202 h = http_class(host, timeout=timeout, proxy_hostport=proxy)
201 self._connections.setdefault(connkey, []).append(h)
203 self._connections.setdefault(connkey, []).append(h)
202
204
203 headers = dict(req.headers)
205 headers = dict(req.headers)
204 headers.update(req.unredirected_hdrs)
206 headers.update(req.unredirected_hdrs)
205 headers = dict(
207 headers = dict(
206 (name.title(), val) for name, val in headers.items())
208 (name.title(), val) for name, val in headers.items())
207 try:
209 try:
208 path = req.get_selector()
210 path = req.get_selector()
209 if '://' in path:
211 if '://' in path:
210 path = path.split('://', 1)[1].split('/', 1)[1]
212 path = path.split('://', 1)[1].split('/', 1)[1]
211 if path[0] != '/':
213 if path[0] != '/':
212 path = '/' + path
214 path = '/' + path
213 h.request(req.get_method(), path, req.data, headers)
215 h.request(req.get_method(), path, req.data, headers)
214 r = h.getresponse()
216 r = h.getresponse()
215 except socket.error as err: # XXX what error?
217 except socket.error as err: # XXX what error?
216 raise urllib2.URLError(err)
218 raise urllib2.URLError(err)
217
219
218 # Pick apart the HTTPResponse object to get the addinfourl
220 # Pick apart the HTTPResponse object to get the addinfourl
219 # object initialized properly.
221 # object initialized properly.
220 r.recv = r.read
222 r.recv = r.read
221
223
222 resp = urllib.addinfourl(r, r.headers, req.get_full_url())
224 resp = urllib.addinfourl(r, r.headers, req.get_full_url())
223 resp.code = r.status
225 resp.code = r.status
224 resp.msg = r.reason
226 resp.msg = r.reason
225 return resp
227 return resp
226
228
227 # httplib always uses the given host/port as the socket connect
229 # httplib always uses the given host/port as the socket connect
228 # target, and then allows full URIs in the request path, which it
230 # target, and then allows full URIs in the request path, which it
229 # then observes and treats as a signal to do proxying instead.
231 # then observes and treats as a signal to do proxying instead.
230 def http_open(self, req):
232 def http_open(self, req):
231 if req.get_full_url().startswith('https'):
233 if req.get_full_url().startswith('https'):
232 return self.https_open(req)
234 return self.https_open(req)
233 def makehttpcon(*args, **kwargs):
235 def makehttpcon(*args, **kwargs):
234 k2 = dict(kwargs)
236 k2 = dict(kwargs)
235 k2['use_ssl'] = False
237 k2['use_ssl'] = False
236 return HTTPConnection(*args, **k2)
238 return HTTPConnection(*args, **k2)
237 return self.do_open(makehttpcon, req, False)
239 return self.do_open(makehttpcon, req, False)
238
240
239 def https_open(self, req):
241 def https_open(self, req):
240 # req.get_full_url() does not contain credentials and we may
242 # req.get_full_url() does not contain credentials and we may
241 # need them to match the certificates.
243 # need them to match the certificates.
242 url = req.get_full_url()
244 url = req.get_full_url()
243 user, password = self.pwmgr.find_stored_password(url)
245 user, password = self.pwmgr.find_stored_password(url)
244 res = readauthforuri(self.ui, url, user)
246 res = readauthforuri(self.ui, url, user)
245 if res:
247 if res:
246 group, auth = res
248 group, auth = res
247 self.auth = auth
249 self.auth = auth
248 self.ui.debug("using auth.%s.* for authentication\n" % group)
250 self.ui.debug("using auth.%s.* for authentication\n" % group)
249 else:
251 else:
250 self.auth = None
252 self.auth = None
251 return self.do_open(self._makesslconnection, req, True)
253 return self.do_open(self._makesslconnection, req, True)
252
254
253 def _makesslconnection(self, host, port=443, *args, **kwargs):
255 def _makesslconnection(self, host, port=443, *args, **kwargs):
254 keyfile = None
256 keyfile = None
255 certfile = None
257 certfile = None
256
258
257 if args: # key_file
259 if args: # key_file
258 keyfile = args.pop(0)
260 keyfile = args.pop(0)
259 if args: # cert_file
261 if args: # cert_file
260 certfile = args.pop(0)
262 certfile = args.pop(0)
261
263
262 # if the user has specified different key/cert files in
264 # if the user has specified different key/cert files in
263 # hgrc, we prefer these
265 # hgrc, we prefer these
264 if self.auth and 'key' in self.auth and 'cert' in self.auth:
266 if self.auth and 'key' in self.auth and 'cert' in self.auth:
265 keyfile = self.auth['key']
267 keyfile = self.auth['key']
266 certfile = self.auth['cert']
268 certfile = self.auth['cert']
267
269
268 # let host port take precedence
270 # let host port take precedence
269 if ':' in host and '[' not in host or ']:' in host:
271 if ':' in host and '[' not in host or ']:' in host:
270 host, port = host.rsplit(':', 1)
272 host, port = host.rsplit(':', 1)
271 port = int(port)
273 port = int(port)
272 if '[' in host:
274 if '[' in host:
273 host = host[1:-1]
275 host = host[1:-1]
274
276
275 kwargs['keyfile'] = keyfile
277 kwargs['keyfile'] = keyfile
276 kwargs['certfile'] = certfile
278 kwargs['certfile'] = certfile
277
279
278 kwargs.update(sslutil.sslkwargs(self.ui, host))
280 kwargs.update(sslutil.sslkwargs(self.ui, host))
279
281
280 con = HTTPConnection(host, port, use_ssl=True,
282 con = HTTPConnection(host, port, use_ssl=True,
281 ssl_wrap_socket=sslutil.wrapsocket,
283 ssl_wrap_socket=sslutil.wrapsocket,
282 ssl_validator=sslutil.validator(self.ui, host),
284 ssl_validator=sslutil.validator(self.ui, host),
283 **kwargs)
285 **kwargs)
284 return con
286 return con
General Comments 0
You need to be logged in to leave comments. Login now