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