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