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