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