##// END OF EJS Templates
httppeer: make __del__ access to self.urlopener more safe...
Mads Kiilerich -
r30241:cac4ca03 stable
parent child Browse files
Show More
@@ -1,308 +1,309 b''
1 # httppeer.py - HTTP repository proxy classes for mercurial
1 # httppeer.py - HTTP repository proxy classes for mercurial
2 #
2 #
3 # Copyright 2005, 2006 Matt Mackall <mpm@selenic.com>
3 # Copyright 2005, 2006 Matt Mackall <mpm@selenic.com>
4 # Copyright 2006 Vadim Gelfer <vadim.gelfer@gmail.com>
4 # Copyright 2006 Vadim Gelfer <vadim.gelfer@gmail.com>
5 #
5 #
6 # This software may be used and distributed according to the terms of the
6 # This software may be used and distributed according to the terms of the
7 # GNU General Public License version 2 or any later version.
7 # GNU General Public License version 2 or any later version.
8
8
9 from __future__ import absolute_import
9 from __future__ import absolute_import
10
10
11 import errno
11 import errno
12 import os
12 import os
13 import socket
13 import socket
14 import tempfile
14 import tempfile
15 import zlib
15 import zlib
16
16
17 from .i18n import _
17 from .i18n import _
18 from .node import nullid
18 from .node import nullid
19 from . import (
19 from . import (
20 bundle2,
20 bundle2,
21 error,
21 error,
22 httpconnection,
22 httpconnection,
23 statichttprepo,
23 statichttprepo,
24 url,
24 url,
25 util,
25 util,
26 wireproto,
26 wireproto,
27 )
27 )
28
28
29 httplib = util.httplib
29 httplib = util.httplib
30 urlerr = util.urlerr
30 urlerr = util.urlerr
31 urlreq = util.urlreq
31 urlreq = util.urlreq
32
32
33 def zgenerator(f):
33 def zgenerator(f):
34 zd = zlib.decompressobj()
34 zd = zlib.decompressobj()
35 try:
35 try:
36 for chunk in util.filechunkiter(f):
36 for chunk in util.filechunkiter(f):
37 while chunk:
37 while chunk:
38 yield zd.decompress(chunk, 2**18)
38 yield zd.decompress(chunk, 2**18)
39 chunk = zd.unconsumed_tail
39 chunk = zd.unconsumed_tail
40 except httplib.HTTPException:
40 except httplib.HTTPException:
41 raise IOError(None, _('connection ended unexpectedly'))
41 raise IOError(None, _('connection ended unexpectedly'))
42 yield zd.flush()
42 yield zd.flush()
43
43
44 class httppeer(wireproto.wirepeer):
44 class httppeer(wireproto.wirepeer):
45 def __init__(self, ui, path):
45 def __init__(self, ui, path):
46 self.path = path
46 self.path = path
47 self.caps = None
47 self.caps = None
48 self.handler = None
48 self.handler = None
49 self.urlopener = None
49 self.urlopener = None
50 self.requestbuilder = None
50 self.requestbuilder = None
51 u = util.url(path)
51 u = util.url(path)
52 if u.query or u.fragment:
52 if u.query or u.fragment:
53 raise error.Abort(_('unsupported URL component: "%s"') %
53 raise error.Abort(_('unsupported URL component: "%s"') %
54 (u.query or u.fragment))
54 (u.query or u.fragment))
55
55
56 # urllib cannot handle URLs with embedded user or passwd
56 # urllib cannot handle URLs with embedded user or passwd
57 self._url, authinfo = u.authinfo()
57 self._url, authinfo = u.authinfo()
58
58
59 self.ui = ui
59 self.ui = ui
60 self.ui.debug('using %s\n' % self._url)
60 self.ui.debug('using %s\n' % self._url)
61
61
62 self.urlopener = url.opener(ui, authinfo)
62 self.urlopener = url.opener(ui, authinfo)
63 self.requestbuilder = urlreq.request
63 self.requestbuilder = urlreq.request
64
64
65 def __del__(self):
65 def __del__(self):
66 if self.urlopener:
66 urlopener = getattr(self, 'urlopener', None)
67 for h in self.urlopener.handlers:
67 if urlopener:
68 for h in urlopener.handlers:
68 h.close()
69 h.close()
69 getattr(h, "close_all", lambda : None)()
70 getattr(h, "close_all", lambda : None)()
70
71
71 def url(self):
72 def url(self):
72 return self.path
73 return self.path
73
74
74 # look up capabilities only when needed
75 # look up capabilities only when needed
75
76
76 def _fetchcaps(self):
77 def _fetchcaps(self):
77 self.caps = set(self._call('capabilities').split())
78 self.caps = set(self._call('capabilities').split())
78
79
79 def _capabilities(self):
80 def _capabilities(self):
80 if self.caps is None:
81 if self.caps is None:
81 try:
82 try:
82 self._fetchcaps()
83 self._fetchcaps()
83 except error.RepoError:
84 except error.RepoError:
84 self.caps = set()
85 self.caps = set()
85 self.ui.debug('capabilities: %s\n' %
86 self.ui.debug('capabilities: %s\n' %
86 (' '.join(self.caps or ['none'])))
87 (' '.join(self.caps or ['none'])))
87 return self.caps
88 return self.caps
88
89
89 def lock(self):
90 def lock(self):
90 raise error.Abort(_('operation not supported over http'))
91 raise error.Abort(_('operation not supported over http'))
91
92
92 def _callstream(self, cmd, **args):
93 def _callstream(self, cmd, **args):
93 if cmd == 'pushkey':
94 if cmd == 'pushkey':
94 args['data'] = ''
95 args['data'] = ''
95 data = args.pop('data', None)
96 data = args.pop('data', None)
96 headers = args.pop('headers', {})
97 headers = args.pop('headers', {})
97
98
98 self.ui.debug("sending %s command\n" % cmd)
99 self.ui.debug("sending %s command\n" % cmd)
99 q = [('cmd', cmd)]
100 q = [('cmd', cmd)]
100 headersize = 0
101 headersize = 0
101 # Important: don't use self.capable() here or else you end up
102 # Important: don't use self.capable() here or else you end up
102 # with infinite recursion when trying to look up capabilities
103 # with infinite recursion when trying to look up capabilities
103 # for the first time.
104 # for the first time.
104 postargsok = self.caps is not None and 'httppostargs' in self.caps
105 postargsok = self.caps is not None and 'httppostargs' in self.caps
105 # TODO: support for httppostargs when data is a file-like
106 # TODO: support for httppostargs when data is a file-like
106 # object rather than a basestring
107 # object rather than a basestring
107 canmungedata = not data or isinstance(data, basestring)
108 canmungedata = not data or isinstance(data, basestring)
108 if postargsok and canmungedata:
109 if postargsok and canmungedata:
109 strargs = urlreq.urlencode(sorted(args.items()))
110 strargs = urlreq.urlencode(sorted(args.items()))
110 if strargs:
111 if strargs:
111 if not data:
112 if not data:
112 data = strargs
113 data = strargs
113 elif isinstance(data, basestring):
114 elif isinstance(data, basestring):
114 data = strargs + data
115 data = strargs + data
115 headers['X-HgArgs-Post'] = len(strargs)
116 headers['X-HgArgs-Post'] = len(strargs)
116 else:
117 else:
117 if len(args) > 0:
118 if len(args) > 0:
118 httpheader = self.capable('httpheader')
119 httpheader = self.capable('httpheader')
119 if httpheader:
120 if httpheader:
120 headersize = int(httpheader.split(',', 1)[0])
121 headersize = int(httpheader.split(',', 1)[0])
121 if headersize > 0:
122 if headersize > 0:
122 # The headers can typically carry more data than the URL.
123 # The headers can typically carry more data than the URL.
123 encargs = urlreq.urlencode(sorted(args.items()))
124 encargs = urlreq.urlencode(sorted(args.items()))
124 headerfmt = 'X-HgArg-%s'
125 headerfmt = 'X-HgArg-%s'
125 contentlen = headersize - len(headerfmt % '000' + ': \r\n')
126 contentlen = headersize - len(headerfmt % '000' + ': \r\n')
126 headernum = 0
127 headernum = 0
127 varyheaders = []
128 varyheaders = []
128 for i in xrange(0, len(encargs), contentlen):
129 for i in xrange(0, len(encargs), contentlen):
129 headernum += 1
130 headernum += 1
130 header = headerfmt % str(headernum)
131 header = headerfmt % str(headernum)
131 headers[header] = encargs[i:i + contentlen]
132 headers[header] = encargs[i:i + contentlen]
132 varyheaders.append(header)
133 varyheaders.append(header)
133 headers['Vary'] = ','.join(varyheaders)
134 headers['Vary'] = ','.join(varyheaders)
134 else:
135 else:
135 q += sorted(args.items())
136 q += sorted(args.items())
136 qs = '?%s' % urlreq.urlencode(q)
137 qs = '?%s' % urlreq.urlencode(q)
137 cu = "%s%s" % (self._url, qs)
138 cu = "%s%s" % (self._url, qs)
138 size = 0
139 size = 0
139 if util.safehasattr(data, 'length'):
140 if util.safehasattr(data, 'length'):
140 size = data.length
141 size = data.length
141 elif data is not None:
142 elif data is not None:
142 size = len(data)
143 size = len(data)
143 if size and self.ui.configbool('ui', 'usehttp2', False):
144 if size and self.ui.configbool('ui', 'usehttp2', False):
144 headers['Expect'] = '100-Continue'
145 headers['Expect'] = '100-Continue'
145 headers['X-HgHttp2'] = '1'
146 headers['X-HgHttp2'] = '1'
146 if data is not None and 'Content-Type' not in headers:
147 if data is not None and 'Content-Type' not in headers:
147 headers['Content-Type'] = 'application/mercurial-0.1'
148 headers['Content-Type'] = 'application/mercurial-0.1'
148 req = self.requestbuilder(cu, data, headers)
149 req = self.requestbuilder(cu, data, headers)
149 if data is not None:
150 if data is not None:
150 self.ui.debug("sending %s bytes\n" % size)
151 self.ui.debug("sending %s bytes\n" % size)
151 req.add_unredirected_header('Content-Length', '%d' % size)
152 req.add_unredirected_header('Content-Length', '%d' % size)
152 try:
153 try:
153 resp = self.urlopener.open(req)
154 resp = self.urlopener.open(req)
154 except urlerr.httperror as inst:
155 except urlerr.httperror as inst:
155 if inst.code == 401:
156 if inst.code == 401:
156 raise error.Abort(_('authorization failed'))
157 raise error.Abort(_('authorization failed'))
157 raise
158 raise
158 except httplib.HTTPException as inst:
159 except httplib.HTTPException as inst:
159 self.ui.debug('http error while sending %s command\n' % cmd)
160 self.ui.debug('http error while sending %s command\n' % cmd)
160 self.ui.traceback()
161 self.ui.traceback()
161 raise IOError(None, inst)
162 raise IOError(None, inst)
162 except IndexError:
163 except IndexError:
163 # this only happens with Python 2.3, later versions raise URLError
164 # this only happens with Python 2.3, later versions raise URLError
164 raise error.Abort(_('http error, possibly caused by proxy setting'))
165 raise error.Abort(_('http error, possibly caused by proxy setting'))
165 # record the url we got redirected to
166 # record the url we got redirected to
166 resp_url = resp.geturl()
167 resp_url = resp.geturl()
167 if resp_url.endswith(qs):
168 if resp_url.endswith(qs):
168 resp_url = resp_url[:-len(qs)]
169 resp_url = resp_url[:-len(qs)]
169 if self._url.rstrip('/') != resp_url.rstrip('/'):
170 if self._url.rstrip('/') != resp_url.rstrip('/'):
170 if not self.ui.quiet:
171 if not self.ui.quiet:
171 self.ui.warn(_('real URL is %s\n') % resp_url)
172 self.ui.warn(_('real URL is %s\n') % resp_url)
172 self._url = resp_url
173 self._url = resp_url
173 try:
174 try:
174 proto = resp.getheader('content-type')
175 proto = resp.getheader('content-type')
175 except AttributeError:
176 except AttributeError:
176 proto = resp.headers.get('content-type', '')
177 proto = resp.headers.get('content-type', '')
177
178
178 safeurl = util.hidepassword(self._url)
179 safeurl = util.hidepassword(self._url)
179 if proto.startswith('application/hg-error'):
180 if proto.startswith('application/hg-error'):
180 raise error.OutOfBandError(resp.read())
181 raise error.OutOfBandError(resp.read())
181 # accept old "text/plain" and "application/hg-changegroup" for now
182 # accept old "text/plain" and "application/hg-changegroup" for now
182 if not (proto.startswith('application/mercurial-') or
183 if not (proto.startswith('application/mercurial-') or
183 (proto.startswith('text/plain')
184 (proto.startswith('text/plain')
184 and not resp.headers.get('content-length')) or
185 and not resp.headers.get('content-length')) or
185 proto.startswith('application/hg-changegroup')):
186 proto.startswith('application/hg-changegroup')):
186 self.ui.debug("requested URL: '%s'\n" % util.hidepassword(cu))
187 self.ui.debug("requested URL: '%s'\n" % util.hidepassword(cu))
187 raise error.RepoError(
188 raise error.RepoError(
188 _("'%s' does not appear to be an hg repository:\n"
189 _("'%s' does not appear to be an hg repository:\n"
189 "---%%<--- (%s)\n%s\n---%%<---\n")
190 "---%%<--- (%s)\n%s\n---%%<---\n")
190 % (safeurl, proto or 'no content-type', resp.read(1024)))
191 % (safeurl, proto or 'no content-type', resp.read(1024)))
191
192
192 if proto.startswith('application/mercurial-'):
193 if proto.startswith('application/mercurial-'):
193 try:
194 try:
194 version = proto.split('-', 1)[1]
195 version = proto.split('-', 1)[1]
195 version_info = tuple([int(n) for n in version.split('.')])
196 version_info = tuple([int(n) for n in version.split('.')])
196 except ValueError:
197 except ValueError:
197 raise error.RepoError(_("'%s' sent a broken Content-Type "
198 raise error.RepoError(_("'%s' sent a broken Content-Type "
198 "header (%s)") % (safeurl, proto))
199 "header (%s)") % (safeurl, proto))
199 if version_info > (0, 1):
200 if version_info > (0, 1):
200 raise error.RepoError(_("'%s' uses newer protocol %s") %
201 raise error.RepoError(_("'%s' uses newer protocol %s") %
201 (safeurl, version))
202 (safeurl, version))
202
203
203 return resp
204 return resp
204
205
205 def _call(self, cmd, **args):
206 def _call(self, cmd, **args):
206 fp = self._callstream(cmd, **args)
207 fp = self._callstream(cmd, **args)
207 try:
208 try:
208 return fp.read()
209 return fp.read()
209 finally:
210 finally:
210 # if using keepalive, allow connection to be reused
211 # if using keepalive, allow connection to be reused
211 fp.close()
212 fp.close()
212
213
213 def _callpush(self, cmd, cg, **args):
214 def _callpush(self, cmd, cg, **args):
214 # have to stream bundle to a temp file because we do not have
215 # have to stream bundle to a temp file because we do not have
215 # http 1.1 chunked transfer.
216 # http 1.1 chunked transfer.
216
217
217 types = self.capable('unbundle')
218 types = self.capable('unbundle')
218 try:
219 try:
219 types = types.split(',')
220 types = types.split(',')
220 except AttributeError:
221 except AttributeError:
221 # servers older than d1b16a746db6 will send 'unbundle' as a
222 # servers older than d1b16a746db6 will send 'unbundle' as a
222 # boolean capability. They only support headerless/uncompressed
223 # boolean capability. They only support headerless/uncompressed
223 # bundles.
224 # bundles.
224 types = [""]
225 types = [""]
225 for x in types:
226 for x in types:
226 if x in bundle2.bundletypes:
227 if x in bundle2.bundletypes:
227 type = x
228 type = x
228 break
229 break
229
230
230 tempname = bundle2.writebundle(self.ui, cg, None, type)
231 tempname = bundle2.writebundle(self.ui, cg, None, type)
231 fp = httpconnection.httpsendfile(self.ui, tempname, "rb")
232 fp = httpconnection.httpsendfile(self.ui, tempname, "rb")
232 headers = {'Content-Type': 'application/mercurial-0.1'}
233 headers = {'Content-Type': 'application/mercurial-0.1'}
233
234
234 try:
235 try:
235 r = self._call(cmd, data=fp, headers=headers, **args)
236 r = self._call(cmd, data=fp, headers=headers, **args)
236 vals = r.split('\n', 1)
237 vals = r.split('\n', 1)
237 if len(vals) < 2:
238 if len(vals) < 2:
238 raise error.ResponseError(_("unexpected response:"), r)
239 raise error.ResponseError(_("unexpected response:"), r)
239 return vals
240 return vals
240 except socket.error as err:
241 except socket.error as err:
241 if err.args[0] in (errno.ECONNRESET, errno.EPIPE):
242 if err.args[0] in (errno.ECONNRESET, errno.EPIPE):
242 raise error.Abort(_('push failed: %s') % err.args[1])
243 raise error.Abort(_('push failed: %s') % err.args[1])
243 raise error.Abort(err.args[1])
244 raise error.Abort(err.args[1])
244 finally:
245 finally:
245 fp.close()
246 fp.close()
246 os.unlink(tempname)
247 os.unlink(tempname)
247
248
248 def _calltwowaystream(self, cmd, fp, **args):
249 def _calltwowaystream(self, cmd, fp, **args):
249 fh = None
250 fh = None
250 fp_ = None
251 fp_ = None
251 filename = None
252 filename = None
252 try:
253 try:
253 # dump bundle to disk
254 # dump bundle to disk
254 fd, filename = tempfile.mkstemp(prefix="hg-bundle-", suffix=".hg")
255 fd, filename = tempfile.mkstemp(prefix="hg-bundle-", suffix=".hg")
255 fh = os.fdopen(fd, "wb")
256 fh = os.fdopen(fd, "wb")
256 d = fp.read(4096)
257 d = fp.read(4096)
257 while d:
258 while d:
258 fh.write(d)
259 fh.write(d)
259 d = fp.read(4096)
260 d = fp.read(4096)
260 fh.close()
261 fh.close()
261 # start http push
262 # start http push
262 fp_ = httpconnection.httpsendfile(self.ui, filename, "rb")
263 fp_ = httpconnection.httpsendfile(self.ui, filename, "rb")
263 headers = {'Content-Type': 'application/mercurial-0.1'}
264 headers = {'Content-Type': 'application/mercurial-0.1'}
264 return self._callstream(cmd, data=fp_, headers=headers, **args)
265 return self._callstream(cmd, data=fp_, headers=headers, **args)
265 finally:
266 finally:
266 if fp_ is not None:
267 if fp_ is not None:
267 fp_.close()
268 fp_.close()
268 if fh is not None:
269 if fh is not None:
269 fh.close()
270 fh.close()
270 os.unlink(filename)
271 os.unlink(filename)
271
272
272 def _callcompressable(self, cmd, **args):
273 def _callcompressable(self, cmd, **args):
273 stream = self._callstream(cmd, **args)
274 stream = self._callstream(cmd, **args)
274 return util.chunkbuffer(zgenerator(stream))
275 return util.chunkbuffer(zgenerator(stream))
275
276
276 def _abort(self, exception):
277 def _abort(self, exception):
277 raise exception
278 raise exception
278
279
279 class httpspeer(httppeer):
280 class httpspeer(httppeer):
280 def __init__(self, ui, path):
281 def __init__(self, ui, path):
281 if not url.has_https:
282 if not url.has_https:
282 raise error.Abort(_('Python support for SSL and HTTPS '
283 raise error.Abort(_('Python support for SSL and HTTPS '
283 'is not installed'))
284 'is not installed'))
284 httppeer.__init__(self, ui, path)
285 httppeer.__init__(self, ui, path)
285
286
286 def instance(ui, path, create):
287 def instance(ui, path, create):
287 if create:
288 if create:
288 raise error.Abort(_('cannot create new http repository'))
289 raise error.Abort(_('cannot create new http repository'))
289 try:
290 try:
290 if path.startswith('https:'):
291 if path.startswith('https:'):
291 inst = httpspeer(ui, path)
292 inst = httpspeer(ui, path)
292 else:
293 else:
293 inst = httppeer(ui, path)
294 inst = httppeer(ui, path)
294 try:
295 try:
295 # Try to do useful work when checking compatibility.
296 # Try to do useful work when checking compatibility.
296 # Usually saves a roundtrip since we want the caps anyway.
297 # Usually saves a roundtrip since we want the caps anyway.
297 inst._fetchcaps()
298 inst._fetchcaps()
298 except error.RepoError:
299 except error.RepoError:
299 # No luck, try older compatibility check.
300 # No luck, try older compatibility check.
300 inst.between([(nullid, nullid)])
301 inst.between([(nullid, nullid)])
301 return inst
302 return inst
302 except error.RepoError as httpexception:
303 except error.RepoError as httpexception:
303 try:
304 try:
304 r = statichttprepo.instance(ui, "static-" + path, create)
305 r = statichttprepo.instance(ui, "static-" + path, create)
305 ui.note(_('(falling back to static-http)\n'))
306 ui.note(_('(falling back to static-http)\n'))
306 return r
307 return r
307 except error.RepoError:
308 except error.RepoError:
308 raise httpexception # use the original http RepoError instead
309 raise httpexception # use the original http RepoError instead
General Comments 0
You need to be logged in to leave comments. Login now