##// END OF EJS Templates
httpsendfile: record progress information during read()...
Augie Fackler -
r13115:bda5f35f default
parent child Browse files
Show More
@@ -1,203 +1,203 b''
1 1 # httprepo.py - HTTP repository proxy classes for mercurial
2 2 #
3 3 # Copyright 2005, 2006 Matt Mackall <mpm@selenic.com>
4 4 # Copyright 2006 Vadim Gelfer <vadim.gelfer@gmail.com>
5 5 #
6 6 # This software may be used and distributed according to the terms of the
7 7 # GNU General Public License version 2 or any later version.
8 8
9 9 from node import nullid
10 10 from i18n import _
11 11 import changegroup, statichttprepo, error, url, util, wireproto
12 12 import os, urllib, urllib2, urlparse, zlib, httplib
13 13 import errno, socket
14 14
15 15 def zgenerator(f):
16 16 zd = zlib.decompressobj()
17 17 try:
18 18 for chunk in util.filechunkiter(f):
19 19 while chunk:
20 20 yield zd.decompress(chunk, 2**18)
21 21 chunk = zd.unconsumed_tail
22 22 except httplib.HTTPException:
23 23 raise IOError(None, _('connection ended unexpectedly'))
24 24 yield zd.flush()
25 25
26 26 class httprepository(wireproto.wirerepository):
27 27 def __init__(self, ui, path):
28 28 self.path = path
29 29 self.caps = None
30 30 self.handler = None
31 31 scheme, netloc, urlpath, query, frag = urlparse.urlsplit(path)
32 32 if query or frag:
33 33 raise util.Abort(_('unsupported URL component: "%s"') %
34 34 (query or frag))
35 35
36 36 # urllib cannot handle URLs with embedded user or passwd
37 37 self._url, authinfo = url.getauthinfo(path)
38 38
39 39 self.ui = ui
40 40 self.ui.debug('using %s\n' % self._url)
41 41
42 42 self.urlopener = url.opener(ui, authinfo)
43 43
44 44 def __del__(self):
45 45 for h in self.urlopener.handlers:
46 46 h.close()
47 47 if hasattr(h, "close_all"):
48 48 h.close_all()
49 49
50 50 def url(self):
51 51 return self.path
52 52
53 53 # look up capabilities only when needed
54 54
55 55 def get_caps(self):
56 56 if self.caps is None:
57 57 try:
58 58 self.caps = set(self._call('capabilities').split())
59 59 except error.RepoError:
60 60 self.caps = set()
61 61 self.ui.debug('capabilities: %s\n' %
62 62 (' '.join(self.caps or ['none'])))
63 63 return self.caps
64 64
65 65 capabilities = property(get_caps)
66 66
67 67 def lock(self):
68 68 raise util.Abort(_('operation not supported over http'))
69 69
70 70 def _callstream(self, cmd, **args):
71 71 if cmd == 'pushkey':
72 72 args['data'] = ''
73 73 data = args.pop('data', None)
74 74 headers = args.pop('headers', {})
75 75 self.ui.debug("sending %s command\n" % cmd)
76 76 q = {"cmd": cmd}
77 77 q.update(args)
78 78 qs = '?%s' % urllib.urlencode(q)
79 79 cu = "%s%s" % (self._url, qs)
80 80 req = urllib2.Request(cu, data, headers)
81 81 if data is not None:
82 82 # len(data) is broken if data doesn't fit into Py_ssize_t
83 83 # add the header ourself to avoid OverflowError
84 84 size = data.__len__()
85 85 self.ui.debug("sending %s bytes\n" % size)
86 86 req.add_unredirected_header('Content-Length', '%d' % size)
87 87 try:
88 88 resp = self.urlopener.open(req)
89 89 except urllib2.HTTPError, inst:
90 90 if inst.code == 401:
91 91 raise util.Abort(_('authorization failed'))
92 92 raise
93 93 except httplib.HTTPException, inst:
94 94 self.ui.debug('http error while sending %s command\n' % cmd)
95 95 self.ui.traceback()
96 96 raise IOError(None, inst)
97 97 except IndexError:
98 98 # this only happens with Python 2.3, later versions raise URLError
99 99 raise util.Abort(_('http error, possibly caused by proxy setting'))
100 100 # record the url we got redirected to
101 101 resp_url = resp.geturl()
102 102 if resp_url.endswith(qs):
103 103 resp_url = resp_url[:-len(qs)]
104 104 if self._url.rstrip('/') != resp_url.rstrip('/'):
105 105 self.ui.status(_('real URL is %s\n') % resp_url)
106 106 self._url = resp_url
107 107 try:
108 108 proto = resp.getheader('content-type')
109 109 except AttributeError:
110 110 proto = resp.headers['content-type']
111 111
112 112 safeurl = url.hidepassword(self._url)
113 113 # accept old "text/plain" and "application/hg-changegroup" for now
114 114 if not (proto.startswith('application/mercurial-') or
115 115 proto.startswith('text/plain') or
116 116 proto.startswith('application/hg-changegroup')):
117 117 self.ui.debug("requested URL: '%s'\n" % url.hidepassword(cu))
118 118 raise error.RepoError(
119 119 _("'%s' does not appear to be an hg repository:\n"
120 120 "---%%<--- (%s)\n%s\n---%%<---\n")
121 121 % (safeurl, proto, resp.read()))
122 122
123 123 if proto.startswith('application/mercurial-'):
124 124 try:
125 125 version = proto.split('-', 1)[1]
126 126 version_info = tuple([int(n) for n in version.split('.')])
127 127 except ValueError:
128 128 raise error.RepoError(_("'%s' sent a broken Content-Type "
129 129 "header (%s)") % (safeurl, proto))
130 130 if version_info > (0, 1):
131 131 raise error.RepoError(_("'%s' uses newer protocol %s") %
132 132 (safeurl, version))
133 133
134 134 return resp
135 135
136 136 def _call(self, cmd, **args):
137 137 fp = self._callstream(cmd, **args)
138 138 try:
139 139 return fp.read()
140 140 finally:
141 141 # if using keepalive, allow connection to be reused
142 142 fp.close()
143 143
144 144 def _callpush(self, cmd, cg, **args):
145 145 # have to stream bundle to a temp file because we do not have
146 146 # http 1.1 chunked transfer.
147 147
148 148 type = ""
149 149 types = self.capable('unbundle')
150 150 # servers older than d1b16a746db6 will send 'unbundle' as a
151 151 # boolean capability
152 152 try:
153 153 types = types.split(',')
154 154 except AttributeError:
155 155 types = [""]
156 156 if types:
157 157 for x in types:
158 158 if x in changegroup.bundletypes:
159 159 type = x
160 160 break
161 161
162 162 tempname = changegroup.writebundle(cg, None, type)
163 fp = url.httpsendfile(tempname, "rb")
163 fp = url.httpsendfile(self.ui, tempname, "rb")
164 164 headers = {'Content-Type': 'application/mercurial-0.1'}
165 165
166 166 try:
167 167 try:
168 168 r = self._call(cmd, data=fp, headers=headers, **args)
169 169 return r.split('\n', 1)
170 170 except socket.error, err:
171 171 if err.args[0] in (errno.ECONNRESET, errno.EPIPE):
172 172 raise util.Abort(_('push failed: %s') % err.args[1])
173 173 raise util.Abort(err.args[1])
174 174 finally:
175 175 fp.close()
176 176 os.unlink(tempname)
177 177
178 178 def _abort(self, exception):
179 179 raise exception
180 180
181 181 def _decompress(self, stream):
182 182 return util.chunkbuffer(zgenerator(stream))
183 183
184 184 class httpsrepository(httprepository):
185 185 def __init__(self, ui, path):
186 186 if not url.has_https:
187 187 raise util.Abort(_('Python support for SSL and HTTPS '
188 188 'is not installed'))
189 189 httprepository.__init__(self, ui, path)
190 190
191 191 def instance(ui, path, create):
192 192 if create:
193 193 raise util.Abort(_('cannot create new http repository'))
194 194 try:
195 195 if path.startswith('https:'):
196 196 inst = httpsrepository(ui, path)
197 197 else:
198 198 inst = httprepository(ui, path)
199 199 inst.between([(nullid, nullid)])
200 200 return inst
201 201 except error.RepoError:
202 202 ui.note('(falling back to static-http)\n')
203 203 return statichttprepo.instance(ui, "static-" + path, create)
@@ -1,698 +1,716 b''
1 1 # url.py - HTTP handling for mercurial
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 #
7 7 # This software may be used and distributed according to the terms of the
8 8 # GNU General Public License version 2 or any later version.
9 9
10 10 import urllib, urllib2, urlparse, httplib, os, re, socket, cStringIO
11 11 import __builtin__
12 12 from i18n import _
13 13 import keepalive, util
14 14
15 15 def _urlunparse(scheme, netloc, path, params, query, fragment, url):
16 16 '''Handle cases where urlunparse(urlparse(x://)) doesn't preserve the "//"'''
17 17 result = urlparse.urlunparse((scheme, netloc, path, params, query, fragment))
18 18 if (scheme and
19 19 result.startswith(scheme + ':') and
20 20 not result.startswith(scheme + '://') and
21 21 url.startswith(scheme + '://')
22 22 ):
23 23 result = scheme + '://' + result[len(scheme + ':'):]
24 24 return result
25 25
26 26 def hidepassword(url):
27 27 '''hide user credential in a url string'''
28 28 scheme, netloc, path, params, query, fragment = urlparse.urlparse(url)
29 29 netloc = re.sub('([^:]*):([^@]*)@(.*)', r'\1:***@\3', netloc)
30 30 return _urlunparse(scheme, netloc, path, params, query, fragment, url)
31 31
32 32 def removeauth(url):
33 33 '''remove all authentication information from a url string'''
34 34 scheme, netloc, path, params, query, fragment = urlparse.urlparse(url)
35 35 netloc = netloc[netloc.find('@')+1:]
36 36 return _urlunparse(scheme, netloc, path, params, query, fragment, url)
37 37
38 38 def netlocsplit(netloc):
39 39 '''split [user[:passwd]@]host[:port] into 4-tuple.'''
40 40
41 41 a = netloc.find('@')
42 42 if a == -1:
43 43 user, passwd = None, None
44 44 else:
45 45 userpass, netloc = netloc[:a], netloc[a + 1:]
46 46 c = userpass.find(':')
47 47 if c == -1:
48 48 user, passwd = urllib.unquote(userpass), None
49 49 else:
50 50 user = urllib.unquote(userpass[:c])
51 51 passwd = urllib.unquote(userpass[c + 1:])
52 52 c = netloc.find(':')
53 53 if c == -1:
54 54 host, port = netloc, None
55 55 else:
56 56 host, port = netloc[:c], netloc[c + 1:]
57 57 return host, port, user, passwd
58 58
59 59 def netlocunsplit(host, port, user=None, passwd=None):
60 60 '''turn host, port, user, passwd into [user[:passwd]@]host[:port].'''
61 61 if port:
62 62 hostport = host + ':' + port
63 63 else:
64 64 hostport = host
65 65 if user:
66 66 quote = lambda s: urllib.quote(s, safe='')
67 67 if passwd:
68 68 userpass = quote(user) + ':' + quote(passwd)
69 69 else:
70 70 userpass = quote(user)
71 71 return userpass + '@' + hostport
72 72 return hostport
73 73
74 74 _safe = ('abcdefghijklmnopqrstuvwxyz'
75 75 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'
76 76 '0123456789' '_.-/')
77 77 _safeset = None
78 78 _hex = None
79 79 def quotepath(path):
80 80 '''quote the path part of a URL
81 81
82 82 This is similar to urllib.quote, but it also tries to avoid
83 83 quoting things twice (inspired by wget):
84 84
85 85 >>> quotepath('abc def')
86 86 'abc%20def'
87 87 >>> quotepath('abc%20def')
88 88 'abc%20def'
89 89 >>> quotepath('abc%20 def')
90 90 'abc%20%20def'
91 91 >>> quotepath('abc def%20')
92 92 'abc%20def%20'
93 93 >>> quotepath('abc def%2')
94 94 'abc%20def%252'
95 95 >>> quotepath('abc def%')
96 96 'abc%20def%25'
97 97 '''
98 98 global _safeset, _hex
99 99 if _safeset is None:
100 100 _safeset = set(_safe)
101 101 _hex = set('abcdefABCDEF0123456789')
102 102 l = list(path)
103 103 for i in xrange(len(l)):
104 104 c = l[i]
105 105 if (c == '%' and i + 2 < len(l) and
106 106 l[i + 1] in _hex and l[i + 2] in _hex):
107 107 pass
108 108 elif c not in _safeset:
109 109 l[i] = '%%%02X' % ord(c)
110 110 return ''.join(l)
111 111
112 112 class passwordmgr(urllib2.HTTPPasswordMgrWithDefaultRealm):
113 113 def __init__(self, ui):
114 114 urllib2.HTTPPasswordMgrWithDefaultRealm.__init__(self)
115 115 self.ui = ui
116 116
117 117 def find_user_password(self, realm, authuri):
118 118 authinfo = urllib2.HTTPPasswordMgrWithDefaultRealm.find_user_password(
119 119 self, realm, authuri)
120 120 user, passwd = authinfo
121 121 if user and passwd:
122 122 self._writedebug(user, passwd)
123 123 return (user, passwd)
124 124
125 125 if not user:
126 126 auth = self.readauthtoken(authuri)
127 127 if auth:
128 128 user, passwd = auth.get('username'), auth.get('password')
129 129 if not user or not passwd:
130 130 if not self.ui.interactive():
131 131 raise util.Abort(_('http authorization required'))
132 132
133 133 self.ui.write(_("http authorization required\n"))
134 134 self.ui.write(_("realm: %s\n") % realm)
135 135 if user:
136 136 self.ui.write(_("user: %s\n") % user)
137 137 else:
138 138 user = self.ui.prompt(_("user:"), default=None)
139 139
140 140 if not passwd:
141 141 passwd = self.ui.getpass()
142 142
143 143 self.add_password(realm, authuri, user, passwd)
144 144 self._writedebug(user, passwd)
145 145 return (user, passwd)
146 146
147 147 def _writedebug(self, user, passwd):
148 148 msg = _('http auth: user %s, password %s\n')
149 149 self.ui.debug(msg % (user, passwd and '*' * len(passwd) or 'not set'))
150 150
151 151 def readauthtoken(self, uri):
152 152 # Read configuration
153 153 config = dict()
154 154 for key, val in self.ui.configitems('auth'):
155 155 if '.' not in key:
156 156 self.ui.warn(_("ignoring invalid [auth] key '%s'\n") % key)
157 157 continue
158 158 group, setting = key.split('.', 1)
159 159 gdict = config.setdefault(group, dict())
160 160 if setting in ('username', 'cert', 'key'):
161 161 val = util.expandpath(val)
162 162 gdict[setting] = val
163 163
164 164 # Find the best match
165 165 scheme, hostpath = uri.split('://', 1)
166 166 bestlen = 0
167 167 bestauth = None
168 168 for auth in config.itervalues():
169 169 prefix = auth.get('prefix')
170 170 if not prefix:
171 171 continue
172 172 p = prefix.split('://', 1)
173 173 if len(p) > 1:
174 174 schemes, prefix = [p[0]], p[1]
175 175 else:
176 176 schemes = (auth.get('schemes') or 'https').split()
177 177 if (prefix == '*' or hostpath.startswith(prefix)) and \
178 178 len(prefix) > bestlen and scheme in schemes:
179 179 bestlen = len(prefix)
180 180 bestauth = auth
181 181 return bestauth
182 182
183 183 class proxyhandler(urllib2.ProxyHandler):
184 184 def __init__(self, ui):
185 185 proxyurl = ui.config("http_proxy", "host") or os.getenv('http_proxy')
186 186 # XXX proxyauthinfo = None
187 187
188 188 if proxyurl:
189 189 # proxy can be proper url or host[:port]
190 190 if not (proxyurl.startswith('http:') or
191 191 proxyurl.startswith('https:')):
192 192 proxyurl = 'http://' + proxyurl + '/'
193 193 snpqf = urlparse.urlsplit(proxyurl)
194 194 proxyscheme, proxynetloc, proxypath, proxyquery, proxyfrag = snpqf
195 195 hpup = netlocsplit(proxynetloc)
196 196
197 197 proxyhost, proxyport, proxyuser, proxypasswd = hpup
198 198 if not proxyuser:
199 199 proxyuser = ui.config("http_proxy", "user")
200 200 proxypasswd = ui.config("http_proxy", "passwd")
201 201
202 202 # see if we should use a proxy for this url
203 203 no_list = ["localhost", "127.0.0.1"]
204 204 no_list.extend([p.lower() for
205 205 p in ui.configlist("http_proxy", "no")])
206 206 no_list.extend([p.strip().lower() for
207 207 p in os.getenv("no_proxy", '').split(',')
208 208 if p.strip()])
209 209 # "http_proxy.always" config is for running tests on localhost
210 210 if ui.configbool("http_proxy", "always"):
211 211 self.no_list = []
212 212 else:
213 213 self.no_list = no_list
214 214
215 215 proxyurl = urlparse.urlunsplit((
216 216 proxyscheme, netlocunsplit(proxyhost, proxyport,
217 217 proxyuser, proxypasswd or ''),
218 218 proxypath, proxyquery, proxyfrag))
219 219 proxies = {'http': proxyurl, 'https': proxyurl}
220 220 ui.debug('proxying through http://%s:%s\n' %
221 221 (proxyhost, proxyport))
222 222 else:
223 223 proxies = {}
224 224
225 225 # urllib2 takes proxy values from the environment and those
226 226 # will take precedence if found, so drop them
227 227 for env in ["HTTP_PROXY", "http_proxy", "no_proxy"]:
228 228 try:
229 229 if env in os.environ:
230 230 del os.environ[env]
231 231 except OSError:
232 232 pass
233 233
234 234 urllib2.ProxyHandler.__init__(self, proxies)
235 235 self.ui = ui
236 236
237 237 def proxy_open(self, req, proxy, type_):
238 238 host = req.get_host().split(':')[0]
239 239 if host in self.no_list:
240 240 return None
241 241
242 242 # work around a bug in Python < 2.4.2
243 243 # (it leaves a "\n" at the end of Proxy-authorization headers)
244 244 baseclass = req.__class__
245 245 class _request(baseclass):
246 246 def add_header(self, key, val):
247 247 if key.lower() == 'proxy-authorization':
248 248 val = val.strip()
249 249 return baseclass.add_header(self, key, val)
250 250 req.__class__ = _request
251 251
252 252 return urllib2.ProxyHandler.proxy_open(self, req, proxy, type_)
253 253
254 254 class httpsendfile(object):
255 255 """This is a wrapper around the objects returned by python's "open".
256 256
257 257 Its purpose is to send file-like objects via HTTP and, to do so, it
258 258 defines a __len__ attribute to feed the Content-Length header.
259 259 """
260 260
261 def __init__(self, *args, **kwargs):
261 def __init__(self, ui, *args, **kwargs):
262 262 # We can't just "self._data = open(*args, **kwargs)" here because there
263 263 # is an "open" function defined in this module that shadows the global
264 264 # one
265 self.ui = ui
265 266 self._data = __builtin__.open(*args, **kwargs)
266 self.read = self._data.read
267 267 self.seek = self._data.seek
268 268 self.close = self._data.close
269 269 self.write = self._data.write
270 self._len = os.fstat(self._data.fileno()).st_size
271 self._pos = 0
272 self._total = len(self) / 1024 * 2
273
274 def read(self, *args, **kwargs):
275 try:
276 ret = self._data.read(*args, **kwargs)
277 except EOFError:
278 self.ui.progress(_('sending'), None)
279 self._pos += len(ret)
280 # We pass double the max for total because we currently have
281 # to send the bundle twice in the case of a server that
282 # requires authentication. Since we can't know until we try
283 # once whether authentication will be required, just lie to
284 # the user and maybe the push succeeds suddenly at 50%.
285 self.ui.progress(_('sending'), self._pos / 1024,
286 unit=_('kb'), total=self._total)
287 return ret
270 288
271 289 def __len__(self):
272 return os.fstat(self._data.fileno()).st_size
290 return self._len
273 291
274 292 def _gen_sendfile(connection):
275 293 def _sendfile(self, data):
276 294 # send a file
277 295 if isinstance(data, httpsendfile):
278 296 # if auth required, some data sent twice, so rewind here
279 297 data.seek(0)
280 298 for chunk in util.filechunkiter(data):
281 299 connection.send(self, chunk)
282 300 else:
283 301 connection.send(self, data)
284 302 return _sendfile
285 303
286 304 has_https = hasattr(urllib2, 'HTTPSHandler')
287 305 if has_https:
288 306 try:
289 307 # avoid using deprecated/broken FakeSocket in python 2.6
290 308 import ssl
291 309 _ssl_wrap_socket = ssl.wrap_socket
292 310 CERT_REQUIRED = ssl.CERT_REQUIRED
293 311 except ImportError:
294 312 CERT_REQUIRED = 2
295 313
296 314 def _ssl_wrap_socket(sock, key_file, cert_file,
297 315 cert_reqs=CERT_REQUIRED, ca_certs=None):
298 316 if ca_certs:
299 317 raise util.Abort(_(
300 318 'certificate checking requires Python 2.6'))
301 319
302 320 ssl = socket.ssl(sock, key_file, cert_file)
303 321 return httplib.FakeSocket(sock, ssl)
304 322
305 323 try:
306 324 _create_connection = socket.create_connection
307 325 except AttributeError:
308 326 _GLOBAL_DEFAULT_TIMEOUT = object()
309 327
310 328 def _create_connection(address, timeout=_GLOBAL_DEFAULT_TIMEOUT,
311 329 source_address=None):
312 330 # lifted from Python 2.6
313 331
314 332 msg = "getaddrinfo returns an empty list"
315 333 host, port = address
316 334 for res in socket.getaddrinfo(host, port, 0, socket.SOCK_STREAM):
317 335 af, socktype, proto, canonname, sa = res
318 336 sock = None
319 337 try:
320 338 sock = socket.socket(af, socktype, proto)
321 339 if timeout is not _GLOBAL_DEFAULT_TIMEOUT:
322 340 sock.settimeout(timeout)
323 341 if source_address:
324 342 sock.bind(source_address)
325 343 sock.connect(sa)
326 344 return sock
327 345
328 346 except socket.error, msg:
329 347 if sock is not None:
330 348 sock.close()
331 349
332 350 raise socket.error, msg
333 351
334 352 class httpconnection(keepalive.HTTPConnection):
335 353 # must be able to send big bundle as stream.
336 354 send = _gen_sendfile(keepalive.HTTPConnection)
337 355
338 356 def connect(self):
339 357 if has_https and self.realhostport: # use CONNECT proxy
340 358 self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
341 359 self.sock.connect((self.host, self.port))
342 360 if _generic_proxytunnel(self):
343 361 # we do not support client x509 certificates
344 362 self.sock = _ssl_wrap_socket(self.sock, None, None)
345 363 else:
346 364 keepalive.HTTPConnection.connect(self)
347 365
348 366 def getresponse(self):
349 367 proxyres = getattr(self, 'proxyres', None)
350 368 if proxyres:
351 369 if proxyres.will_close:
352 370 self.close()
353 371 self.proxyres = None
354 372 return proxyres
355 373 return keepalive.HTTPConnection.getresponse(self)
356 374
357 375 # general transaction handler to support different ways to handle
358 376 # HTTPS proxying before and after Python 2.6.3.
359 377 def _generic_start_transaction(handler, h, req):
360 378 if hasattr(req, '_tunnel_host') and req._tunnel_host:
361 379 tunnel_host = req._tunnel_host
362 380 if tunnel_host[:7] not in ['http://', 'https:/']:
363 381 tunnel_host = 'https://' + tunnel_host
364 382 new_tunnel = True
365 383 else:
366 384 tunnel_host = req.get_selector()
367 385 new_tunnel = False
368 386
369 387 if new_tunnel or tunnel_host == req.get_full_url(): # has proxy
370 388 urlparts = urlparse.urlparse(tunnel_host)
371 389 if new_tunnel or urlparts[0] == 'https': # only use CONNECT for HTTPS
372 390 realhostport = urlparts[1]
373 391 if realhostport[-1] == ']' or ':' not in realhostport:
374 392 realhostport += ':443'
375 393
376 394 h.realhostport = realhostport
377 395 h.headers = req.headers.copy()
378 396 h.headers.update(handler.parent.addheaders)
379 397 return
380 398
381 399 h.realhostport = None
382 400 h.headers = None
383 401
384 402 def _generic_proxytunnel(self):
385 403 proxyheaders = dict(
386 404 [(x, self.headers[x]) for x in self.headers
387 405 if x.lower().startswith('proxy-')])
388 406 self._set_hostport(self.host, self.port)
389 407 self.send('CONNECT %s HTTP/1.0\r\n' % self.realhostport)
390 408 for header in proxyheaders.iteritems():
391 409 self.send('%s: %s\r\n' % header)
392 410 self.send('\r\n')
393 411
394 412 # majority of the following code is duplicated from
395 413 # httplib.HTTPConnection as there are no adequate places to
396 414 # override functions to provide the needed functionality
397 415 res = self.response_class(self.sock,
398 416 strict=self.strict,
399 417 method=self._method)
400 418
401 419 while True:
402 420 version, status, reason = res._read_status()
403 421 if status != httplib.CONTINUE:
404 422 break
405 423 while True:
406 424 skip = res.fp.readline().strip()
407 425 if not skip:
408 426 break
409 427 res.status = status
410 428 res.reason = reason.strip()
411 429
412 430 if res.status == 200:
413 431 while True:
414 432 line = res.fp.readline()
415 433 if line == '\r\n':
416 434 break
417 435 return True
418 436
419 437 if version == 'HTTP/1.0':
420 438 res.version = 10
421 439 elif version.startswith('HTTP/1.'):
422 440 res.version = 11
423 441 elif version == 'HTTP/0.9':
424 442 res.version = 9
425 443 else:
426 444 raise httplib.UnknownProtocol(version)
427 445
428 446 if res.version == 9:
429 447 res.length = None
430 448 res.chunked = 0
431 449 res.will_close = 1
432 450 res.msg = httplib.HTTPMessage(cStringIO.StringIO())
433 451 return False
434 452
435 453 res.msg = httplib.HTTPMessage(res.fp)
436 454 res.msg.fp = None
437 455
438 456 # are we using the chunked-style of transfer encoding?
439 457 trenc = res.msg.getheader('transfer-encoding')
440 458 if trenc and trenc.lower() == "chunked":
441 459 res.chunked = 1
442 460 res.chunk_left = None
443 461 else:
444 462 res.chunked = 0
445 463
446 464 # will the connection close at the end of the response?
447 465 res.will_close = res._check_close()
448 466
449 467 # do we have a Content-Length?
450 468 # NOTE: RFC 2616, S4.4, #3 says we ignore this if tr_enc is "chunked"
451 469 length = res.msg.getheader('content-length')
452 470 if length and not res.chunked:
453 471 try:
454 472 res.length = int(length)
455 473 except ValueError:
456 474 res.length = None
457 475 else:
458 476 if res.length < 0: # ignore nonsensical negative lengths
459 477 res.length = None
460 478 else:
461 479 res.length = None
462 480
463 481 # does the body have a fixed length? (of zero)
464 482 if (status == httplib.NO_CONTENT or status == httplib.NOT_MODIFIED or
465 483 100 <= status < 200 or # 1xx codes
466 484 res._method == 'HEAD'):
467 485 res.length = 0
468 486
469 487 # if the connection remains open, and we aren't using chunked, and
470 488 # a content-length was not provided, then assume that the connection
471 489 # WILL close.
472 490 if (not res.will_close and
473 491 not res.chunked and
474 492 res.length is None):
475 493 res.will_close = 1
476 494
477 495 self.proxyres = res
478 496
479 497 return False
480 498
481 499 class httphandler(keepalive.HTTPHandler):
482 500 def http_open(self, req):
483 501 return self.do_open(httpconnection, req)
484 502
485 503 def _start_transaction(self, h, req):
486 504 _generic_start_transaction(self, h, req)
487 505 return keepalive.HTTPHandler._start_transaction(self, h, req)
488 506
489 507 def _verifycert(cert, hostname):
490 508 '''Verify that cert (in socket.getpeercert() format) matches hostname.
491 509 CRLs and subjectAltName are not handled.
492 510
493 511 Returns error message if any problems are found and None on success.
494 512 '''
495 513 if not cert:
496 514 return _('no certificate received')
497 515 dnsname = hostname.lower()
498 516 for s in cert.get('subject', []):
499 517 key, value = s[0]
500 518 if key == 'commonName':
501 519 certname = value.lower()
502 520 if (certname == dnsname or
503 521 '.' in dnsname and certname == '*.' + dnsname.split('.', 1)[1]):
504 522 return None
505 523 return _('certificate is for %s') % certname
506 524 return _('no commonName found in certificate')
507 525
508 526 if has_https:
509 527 class BetterHTTPS(httplib.HTTPSConnection):
510 528 send = keepalive.safesend
511 529
512 530 def connect(self):
513 531 if hasattr(self, 'ui'):
514 532 cacerts = self.ui.config('web', 'cacerts')
515 533 else:
516 534 cacerts = None
517 535
518 536 if cacerts:
519 537 sock = _create_connection((self.host, self.port))
520 538 self.sock = _ssl_wrap_socket(sock, self.key_file,
521 539 self.cert_file, cert_reqs=CERT_REQUIRED,
522 540 ca_certs=cacerts)
523 541 msg = _verifycert(self.sock.getpeercert(), self.host)
524 542 if msg:
525 543 raise util.Abort(_('%s certificate error: %s') %
526 544 (self.host, msg))
527 545 self.ui.debug('%s certificate successfully verified\n' %
528 546 self.host)
529 547 else:
530 548 httplib.HTTPSConnection.connect(self)
531 549
532 550 class httpsconnection(BetterHTTPS):
533 551 response_class = keepalive.HTTPResponse
534 552 # must be able to send big bundle as stream.
535 553 send = _gen_sendfile(BetterHTTPS)
536 554 getresponse = keepalive.wrapgetresponse(httplib.HTTPSConnection)
537 555
538 556 def connect(self):
539 557 if self.realhostport: # use CONNECT proxy
540 558 self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
541 559 self.sock.connect((self.host, self.port))
542 560 if _generic_proxytunnel(self):
543 561 self.sock = _ssl_wrap_socket(self.sock, self.key_file,
544 562 self.cert_file)
545 563 else:
546 564 BetterHTTPS.connect(self)
547 565
548 566 class httpshandler(keepalive.KeepAliveHandler, urllib2.HTTPSHandler):
549 567 def __init__(self, ui):
550 568 keepalive.KeepAliveHandler.__init__(self)
551 569 urllib2.HTTPSHandler.__init__(self)
552 570 self.ui = ui
553 571 self.pwmgr = passwordmgr(self.ui)
554 572
555 573 def _start_transaction(self, h, req):
556 574 _generic_start_transaction(self, h, req)
557 575 return keepalive.KeepAliveHandler._start_transaction(self, h, req)
558 576
559 577 def https_open(self, req):
560 578 self.auth = self.pwmgr.readauthtoken(req.get_full_url())
561 579 return self.do_open(self._makeconnection, req)
562 580
563 581 def _makeconnection(self, host, port=None, *args, **kwargs):
564 582 keyfile = None
565 583 certfile = None
566 584
567 585 if len(args) >= 1: # key_file
568 586 keyfile = args[0]
569 587 if len(args) >= 2: # cert_file
570 588 certfile = args[1]
571 589 args = args[2:]
572 590
573 591 # if the user has specified different key/cert files in
574 592 # hgrc, we prefer these
575 593 if self.auth and 'key' in self.auth and 'cert' in self.auth:
576 594 keyfile = self.auth['key']
577 595 certfile = self.auth['cert']
578 596
579 597 conn = httpsconnection(host, port, keyfile, certfile, *args, **kwargs)
580 598 conn.ui = self.ui
581 599 return conn
582 600
583 601 class httpdigestauthhandler(urllib2.HTTPDigestAuthHandler):
584 602 def __init__(self, *args, **kwargs):
585 603 urllib2.HTTPDigestAuthHandler.__init__(self, *args, **kwargs)
586 604 self.retried_req = None
587 605
588 606 def reset_retry_count(self):
589 607 # Python 2.6.5 will call this on 401 or 407 errors and thus loop
590 608 # forever. We disable reset_retry_count completely and reset in
591 609 # http_error_auth_reqed instead.
592 610 pass
593 611
594 612 def http_error_auth_reqed(self, auth_header, host, req, headers):
595 613 # Reset the retry counter once for each request.
596 614 if req is not self.retried_req:
597 615 self.retried_req = req
598 616 self.retried = 0
599 617 # In python < 2.5 AbstractDigestAuthHandler raises a ValueError if
600 618 # it doesn't know about the auth type requested. This can happen if
601 619 # somebody is using BasicAuth and types a bad password.
602 620 try:
603 621 return urllib2.HTTPDigestAuthHandler.http_error_auth_reqed(
604 622 self, auth_header, host, req, headers)
605 623 except ValueError, inst:
606 624 arg = inst.args[0]
607 625 if arg.startswith("AbstractDigestAuthHandler doesn't know "):
608 626 return
609 627 raise
610 628
611 629 class httpbasicauthhandler(urllib2.HTTPBasicAuthHandler):
612 630 def __init__(self, *args, **kwargs):
613 631 urllib2.HTTPBasicAuthHandler.__init__(self, *args, **kwargs)
614 632 self.retried_req = None
615 633
616 634 def reset_retry_count(self):
617 635 # Python 2.6.5 will call this on 401 or 407 errors and thus loop
618 636 # forever. We disable reset_retry_count completely and reset in
619 637 # http_error_auth_reqed instead.
620 638 pass
621 639
622 640 def http_error_auth_reqed(self, auth_header, host, req, headers):
623 641 # Reset the retry counter once for each request.
624 642 if req is not self.retried_req:
625 643 self.retried_req = req
626 644 self.retried = 0
627 645 return urllib2.HTTPBasicAuthHandler.http_error_auth_reqed(
628 646 self, auth_header, host, req, headers)
629 647
630 648 def getauthinfo(path):
631 649 scheme, netloc, urlpath, query, frag = urlparse.urlsplit(path)
632 650 if not urlpath:
633 651 urlpath = '/'
634 652 if scheme != 'file':
635 653 # XXX: why are we quoting the path again with some smart
636 654 # heuristic here? Anyway, it cannot be done with file://
637 655 # urls since path encoding is os/fs dependent (see
638 656 # urllib.pathname2url() for details).
639 657 urlpath = quotepath(urlpath)
640 658 host, port, user, passwd = netlocsplit(netloc)
641 659
642 660 # urllib cannot handle URLs with embedded user or passwd
643 661 url = urlparse.urlunsplit((scheme, netlocunsplit(host, port),
644 662 urlpath, query, frag))
645 663 if user:
646 664 netloc = host
647 665 if port:
648 666 netloc += ':' + port
649 667 # Python < 2.4.3 uses only the netloc to search for a password
650 668 authinfo = (None, (url, netloc), user, passwd or '')
651 669 else:
652 670 authinfo = None
653 671 return url, authinfo
654 672
655 673 handlerfuncs = []
656 674
657 675 def opener(ui, authinfo=None):
658 676 '''
659 677 construct an opener suitable for urllib2
660 678 authinfo will be added to the password manager
661 679 '''
662 680 handlers = [httphandler()]
663 681 if has_https:
664 682 handlers.append(httpshandler(ui))
665 683
666 684 handlers.append(proxyhandler(ui))
667 685
668 686 passmgr = passwordmgr(ui)
669 687 if authinfo is not None:
670 688 passmgr.add_password(*authinfo)
671 689 user, passwd = authinfo[2:4]
672 690 ui.debug('http auth: user %s, password %s\n' %
673 691 (user, passwd and '*' * len(passwd) or 'not set'))
674 692
675 693 handlers.extend((httpbasicauthhandler(passmgr),
676 694 httpdigestauthhandler(passmgr)))
677 695 handlers.extend([h(ui, passmgr) for h in handlerfuncs])
678 696 opener = urllib2.build_opener(*handlers)
679 697
680 698 # 1.0 here is the _protocol_ version
681 699 opener.addheaders = [('User-agent', 'mercurial/proto-1.0')]
682 700 opener.addheaders.append(('Accept', 'application/mercurial-0.1'))
683 701 return opener
684 702
685 703 scheme_re = re.compile(r'^([a-zA-Z0-9+-.]+)://')
686 704
687 705 def open(ui, url, data=None):
688 706 scheme = None
689 707 m = scheme_re.search(url)
690 708 if m:
691 709 scheme = m.group(1).lower()
692 710 if not scheme:
693 711 path = util.normpath(os.path.abspath(url))
694 712 url = 'file://' + urllib.pathname2url(path)
695 713 authinfo = None
696 714 else:
697 715 url, authinfo = getauthinfo(url)
698 716 return opener(ui, authinfo).open(url, data)
General Comments 0
You need to be logged in to leave comments. Login now