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