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