##// END OF EJS Templates
httppeer: consolidate _requestbuilder assignments and document...
Gregory Szorc -
r36979:586891c5 default
parent child Browse files
Show More
@@ -1,503 +1,504 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 io
13 13 import os
14 14 import socket
15 15 import struct
16 16 import tempfile
17 17
18 18 from .i18n import _
19 19 from . import (
20 20 bundle2,
21 21 error,
22 22 httpconnection,
23 23 pycompat,
24 24 statichttprepo,
25 25 url as urlmod,
26 26 util,
27 27 wireproto,
28 28 )
29 29
30 30 httplib = util.httplib
31 31 urlerr = util.urlerr
32 32 urlreq = util.urlreq
33 33
34 34 def encodevalueinheaders(value, header, limit):
35 35 """Encode a string value into multiple HTTP headers.
36 36
37 37 ``value`` will be encoded into 1 or more HTTP headers with the names
38 38 ``header-<N>`` where ``<N>`` is an integer starting at 1. Each header
39 39 name + value will be at most ``limit`` bytes long.
40 40
41 41 Returns an iterable of 2-tuples consisting of header names and
42 42 values as native strings.
43 43 """
44 44 # HTTP Headers are ASCII. Python 3 requires them to be unicodes,
45 45 # not bytes. This function always takes bytes in as arguments.
46 46 fmt = pycompat.strurl(header) + r'-%s'
47 47 # Note: it is *NOT* a bug that the last bit here is a bytestring
48 48 # and not a unicode: we're just getting the encoded length anyway,
49 49 # and using an r-string to make it portable between Python 2 and 3
50 50 # doesn't work because then the \r is a literal backslash-r
51 51 # instead of a carriage return.
52 52 valuelen = limit - len(fmt % r'000') - len(': \r\n')
53 53 result = []
54 54
55 55 n = 0
56 56 for i in xrange(0, len(value), valuelen):
57 57 n += 1
58 58 result.append((fmt % str(n), pycompat.strurl(value[i:i + valuelen])))
59 59
60 60 return result
61 61
62 62 def _wraphttpresponse(resp):
63 63 """Wrap an HTTPResponse with common error handlers.
64 64
65 65 This ensures that any I/O from any consumer raises the appropriate
66 66 error and messaging.
67 67 """
68 68 origread = resp.read
69 69
70 70 class readerproxy(resp.__class__):
71 71 def read(self, size=None):
72 72 try:
73 73 return origread(size)
74 74 except httplib.IncompleteRead as e:
75 75 # e.expected is an integer if length known or None otherwise.
76 76 if e.expected:
77 77 msg = _('HTTP request error (incomplete response; '
78 78 'expected %d bytes got %d)') % (e.expected,
79 79 len(e.partial))
80 80 else:
81 81 msg = _('HTTP request error (incomplete response)')
82 82
83 83 raise error.PeerTransportError(
84 84 msg,
85 85 hint=_('this may be an intermittent network failure; '
86 86 'if the error persists, consider contacting the '
87 87 'network or server operator'))
88 88 except httplib.HTTPException as e:
89 89 raise error.PeerTransportError(
90 90 _('HTTP request error (%s)') % e,
91 91 hint=_('this may be an intermittent network failure; '
92 92 'if the error persists, consider contacting the '
93 93 'network or server operator'))
94 94
95 95 resp.__class__ = readerproxy
96 96
97 97 class _multifile(object):
98 98 def __init__(self, *fileobjs):
99 99 for f in fileobjs:
100 100 if not util.safehasattr(f, 'length'):
101 101 raise ValueError(
102 102 '_multifile only supports file objects that '
103 103 'have a length but this one does not:', type(f), f)
104 104 self._fileobjs = fileobjs
105 105 self._index = 0
106 106
107 107 @property
108 108 def length(self):
109 109 return sum(f.length for f in self._fileobjs)
110 110
111 111 def read(self, amt=None):
112 112 if amt <= 0:
113 113 return ''.join(f.read() for f in self._fileobjs)
114 114 parts = []
115 115 while amt and self._index < len(self._fileobjs):
116 116 parts.append(self._fileobjs[self._index].read(amt))
117 117 got = len(parts[-1])
118 118 if got < amt:
119 119 self._index += 1
120 120 amt -= got
121 121 return ''.join(parts)
122 122
123 123 def seek(self, offset, whence=os.SEEK_SET):
124 124 if whence != os.SEEK_SET:
125 125 raise NotImplementedError(
126 126 '_multifile does not support anything other'
127 127 ' than os.SEEK_SET for whence on seek()')
128 128 if offset != 0:
129 129 raise NotImplementedError(
130 130 '_multifile only supports seeking to start, but that '
131 131 'could be fixed if you need it')
132 132 for f in self._fileobjs:
133 133 f.seek(0)
134 134 self._index = 0
135 135
136 136 class httppeer(wireproto.wirepeer):
137 137 def __init__(self, ui, path):
138 138 self._path = path
139 139 self._caps = None
140 140 self._urlopener = None
141 self._requestbuilder = None
141 # This is an its own attribute to facilitate extensions overriding
142 # the default type.
143 self._requestbuilder = urlreq.request
142 144 u = util.url(path)
143 145 if u.query or u.fragment:
144 146 raise error.Abort(_('unsupported URL component: "%s"') %
145 147 (u.query or u.fragment))
146 148
147 149 # urllib cannot handle URLs with embedded user or passwd
148 150 self._url, authinfo = u.authinfo()
149 151
150 152 self._ui = ui
151 153 ui.debug('using %s\n' % self._url)
152 154
153 155 self._urlopener = urlmod.opener(ui, authinfo)
154 self._requestbuilder = urlreq.request
155 156
156 157 def __del__(self):
157 158 urlopener = getattr(self, '_urlopener', None)
158 159 if urlopener:
159 160 for h in urlopener.handlers:
160 161 h.close()
161 162 getattr(h, "close_all", lambda: None)()
162 163
163 164 def _openurl(self, req):
164 165 if (self._ui.debugflag
165 166 and self._ui.configbool('devel', 'debug.peer-request')):
166 167 dbg = self._ui.debug
167 168 line = 'devel-peer-request: %s\n'
168 169 dbg(line % '%s %s' % (req.get_method(), req.get_full_url()))
169 170 hgargssize = None
170 171
171 172 for header, value in sorted(req.header_items()):
172 173 if header.startswith('X-hgarg-'):
173 174 if hgargssize is None:
174 175 hgargssize = 0
175 176 hgargssize += len(value)
176 177 else:
177 178 dbg(line % ' %s %s' % (header, value))
178 179
179 180 if hgargssize is not None:
180 181 dbg(line % ' %d bytes of commands arguments in headers'
181 182 % hgargssize)
182 183
183 184 if req.has_data():
184 185 data = req.get_data()
185 186 length = getattr(data, 'length', None)
186 187 if length is None:
187 188 length = len(data)
188 189 dbg(line % ' %d bytes of data' % length)
189 190
190 191 start = util.timer()
191 192
192 193 ret = self._urlopener.open(req)
193 194 if self._ui.configbool('devel', 'debug.peer-request'):
194 195 dbg(line % ' finished in %.4f seconds (%s)'
195 196 % (util.timer() - start, ret.code))
196 197 return ret
197 198
198 199 # Begin of _basepeer interface.
199 200
200 201 @util.propertycache
201 202 def ui(self):
202 203 return self._ui
203 204
204 205 def url(self):
205 206 return self._path
206 207
207 208 def local(self):
208 209 return None
209 210
210 211 def peer(self):
211 212 return self
212 213
213 214 def canpush(self):
214 215 return True
215 216
216 217 def close(self):
217 218 pass
218 219
219 220 # End of _basepeer interface.
220 221
221 222 # Begin of _basewirepeer interface.
222 223
223 224 def capabilities(self):
224 225 # self._fetchcaps() should have been called as part of peer
225 226 # handshake. So self._caps should always be set.
226 227 assert self._caps is not None
227 228 return self._caps
228 229
229 230 # End of _basewirepeer interface.
230 231
231 232 # look up capabilities only when needed
232 233
233 234 def _fetchcaps(self):
234 235 self._caps = set(self._call('capabilities').split())
235 236
236 237 def _callstream(self, cmd, _compressible=False, **args):
237 238 args = pycompat.byteskwargs(args)
238 239 if cmd == 'pushkey':
239 240 args['data'] = ''
240 241 data = args.pop('data', None)
241 242 headers = args.pop('headers', {})
242 243
243 244 self.ui.debug("sending %s command\n" % cmd)
244 245 q = [('cmd', cmd)]
245 246 headersize = 0
246 247 varyheaders = []
247 248 # Important: don't use self.capable() here or else you end up
248 249 # with infinite recursion when trying to look up capabilities
249 250 # for the first time.
250 251 postargsok = self._caps is not None and 'httppostargs' in self._caps
251 252
252 253 # Send arguments via POST.
253 254 if postargsok and args:
254 255 strargs = urlreq.urlencode(sorted(args.items()))
255 256 if not data:
256 257 data = strargs
257 258 else:
258 259 if isinstance(data, bytes):
259 260 i = io.BytesIO(data)
260 261 i.length = len(data)
261 262 data = i
262 263 argsio = io.BytesIO(strargs)
263 264 argsio.length = len(strargs)
264 265 data = _multifile(argsio, data)
265 266 headers[r'X-HgArgs-Post'] = len(strargs)
266 267 elif args:
267 268 # Calling self.capable() can infinite loop if we are calling
268 269 # "capabilities". But that command should never accept wire
269 270 # protocol arguments. So this should never happen.
270 271 assert cmd != 'capabilities'
271 272 httpheader = self.capable('httpheader')
272 273 if httpheader:
273 274 headersize = int(httpheader.split(',', 1)[0])
274 275
275 276 # Send arguments via HTTP headers.
276 277 if headersize > 0:
277 278 # The headers can typically carry more data than the URL.
278 279 encargs = urlreq.urlencode(sorted(args.items()))
279 280 for header, value in encodevalueinheaders(encargs, 'X-HgArg',
280 281 headersize):
281 282 headers[header] = value
282 283 varyheaders.append(header)
283 284 # Send arguments via query string (Mercurial <1.9).
284 285 else:
285 286 q += sorted(args.items())
286 287
287 288 qs = '?%s' % urlreq.urlencode(q)
288 289 cu = "%s%s" % (self._url, qs)
289 290 size = 0
290 291 if util.safehasattr(data, 'length'):
291 292 size = data.length
292 293 elif data is not None:
293 294 size = len(data)
294 295 if data is not None and r'Content-Type' not in headers:
295 296 headers[r'Content-Type'] = r'application/mercurial-0.1'
296 297
297 298 # Tell the server we accept application/mercurial-0.2 and multiple
298 299 # compression formats if the server is capable of emitting those
299 300 # payloads.
300 301 protoparams = []
301 302
302 303 mediatypes = set()
303 304 if self._caps is not None:
304 305 mt = self.capable('httpmediatype')
305 306 if mt:
306 307 protoparams.append('0.1')
307 308 mediatypes = set(mt.split(','))
308 309
309 310 if '0.2tx' in mediatypes:
310 311 protoparams.append('0.2')
311 312
312 313 if '0.2tx' in mediatypes and self.capable('compression'):
313 314 # We /could/ compare supported compression formats and prune
314 315 # non-mutually supported or error if nothing is mutually supported.
315 316 # For now, send the full list to the server and have it error.
316 317 comps = [e.wireprotosupport().name for e in
317 318 util.compengines.supportedwireengines(util.CLIENTROLE)]
318 319 protoparams.append('comp=%s' % ','.join(comps))
319 320
320 321 if protoparams:
321 322 protoheaders = encodevalueinheaders(' '.join(protoparams),
322 323 'X-HgProto',
323 324 headersize or 1024)
324 325 for header, value in protoheaders:
325 326 headers[header] = value
326 327 varyheaders.append(header)
327 328
328 329 if varyheaders:
329 330 headers[r'Vary'] = r','.join(varyheaders)
330 331
331 332 req = self._requestbuilder(pycompat.strurl(cu), data, headers)
332 333
333 334 if data is not None:
334 335 self.ui.debug("sending %d bytes\n" % size)
335 336 req.add_unredirected_header(r'Content-Length', r'%d' % size)
336 337 try:
337 338 resp = self._openurl(req)
338 339 except urlerr.httperror as inst:
339 340 if inst.code == 401:
340 341 raise error.Abort(_('authorization failed'))
341 342 raise
342 343 except httplib.HTTPException as inst:
343 344 self.ui.debug('http error while sending %s command\n' % cmd)
344 345 self.ui.traceback()
345 346 raise IOError(None, inst)
346 347
347 348 # Insert error handlers for common I/O failures.
348 349 _wraphttpresponse(resp)
349 350
350 351 # record the url we got redirected to
351 352 resp_url = pycompat.bytesurl(resp.geturl())
352 353 if resp_url.endswith(qs):
353 354 resp_url = resp_url[:-len(qs)]
354 355 if self._url.rstrip('/') != resp_url.rstrip('/'):
355 356 if not self.ui.quiet:
356 357 self.ui.warn(_('real URL is %s\n') % resp_url)
357 358 self._url = resp_url
358 359 try:
359 360 proto = pycompat.bytesurl(resp.getheader(r'content-type', r''))
360 361 except AttributeError:
361 362 proto = pycompat.bytesurl(resp.headers.get(r'content-type', r''))
362 363
363 364 safeurl = util.hidepassword(self._url)
364 365 if proto.startswith('application/hg-error'):
365 366 raise error.OutOfBandError(resp.read())
366 367 # accept old "text/plain" and "application/hg-changegroup" for now
367 368 if not (proto.startswith('application/mercurial-') or
368 369 (proto.startswith('text/plain')
369 370 and not resp.headers.get('content-length')) or
370 371 proto.startswith('application/hg-changegroup')):
371 372 self.ui.debug("requested URL: '%s'\n" % util.hidepassword(cu))
372 373 raise error.RepoError(
373 374 _("'%s' does not appear to be an hg repository:\n"
374 375 "---%%<--- (%s)\n%s\n---%%<---\n")
375 376 % (safeurl, proto or 'no content-type', resp.read(1024)))
376 377
377 378 if proto.startswith('application/mercurial-'):
378 379 try:
379 380 version = proto.split('-', 1)[1]
380 381 version_info = tuple([int(n) for n in version.split('.')])
381 382 except ValueError:
382 383 raise error.RepoError(_("'%s' sent a broken Content-Type "
383 384 "header (%s)") % (safeurl, proto))
384 385
385 386 # TODO consider switching to a decompression reader that uses
386 387 # generators.
387 388 if version_info == (0, 1):
388 389 if _compressible:
389 390 return util.compengines['zlib'].decompressorreader(resp)
390 391 return resp
391 392 elif version_info == (0, 2):
392 393 # application/mercurial-0.2 always identifies the compression
393 394 # engine in the payload header.
394 395 elen = struct.unpack('B', resp.read(1))[0]
395 396 ename = resp.read(elen)
396 397 engine = util.compengines.forwiretype(ename)
397 398 return engine.decompressorreader(resp)
398 399 else:
399 400 raise error.RepoError(_("'%s' uses newer protocol %s") %
400 401 (safeurl, version))
401 402
402 403 if _compressible:
403 404 return util.compengines['zlib'].decompressorreader(resp)
404 405
405 406 return resp
406 407
407 408 def _call(self, cmd, **args):
408 409 fp = self._callstream(cmd, **args)
409 410 try:
410 411 return fp.read()
411 412 finally:
412 413 # if using keepalive, allow connection to be reused
413 414 fp.close()
414 415
415 416 def _callpush(self, cmd, cg, **args):
416 417 # have to stream bundle to a temp file because we do not have
417 418 # http 1.1 chunked transfer.
418 419
419 420 types = self.capable('unbundle')
420 421 try:
421 422 types = types.split(',')
422 423 except AttributeError:
423 424 # servers older than d1b16a746db6 will send 'unbundle' as a
424 425 # boolean capability. They only support headerless/uncompressed
425 426 # bundles.
426 427 types = [""]
427 428 for x in types:
428 429 if x in bundle2.bundletypes:
429 430 type = x
430 431 break
431 432
432 433 tempname = bundle2.writebundle(self.ui, cg, None, type)
433 434 fp = httpconnection.httpsendfile(self.ui, tempname, "rb")
434 435 headers = {r'Content-Type': r'application/mercurial-0.1'}
435 436
436 437 try:
437 438 r = self._call(cmd, data=fp, headers=headers, **args)
438 439 vals = r.split('\n', 1)
439 440 if len(vals) < 2:
440 441 raise error.ResponseError(_("unexpected response:"), r)
441 442 return vals
442 443 except urlerr.httperror:
443 444 # Catch and re-raise these so we don't try and treat them
444 445 # like generic socket errors. They lack any values in
445 446 # .args on Python 3 which breaks our socket.error block.
446 447 raise
447 448 except socket.error as err:
448 449 if err.args[0] in (errno.ECONNRESET, errno.EPIPE):
449 450 raise error.Abort(_('push failed: %s') % err.args[1])
450 451 raise error.Abort(err.args[1])
451 452 finally:
452 453 fp.close()
453 454 os.unlink(tempname)
454 455
455 456 def _calltwowaystream(self, cmd, fp, **args):
456 457 fh = None
457 458 fp_ = None
458 459 filename = None
459 460 try:
460 461 # dump bundle to disk
461 462 fd, filename = tempfile.mkstemp(prefix="hg-bundle-", suffix=".hg")
462 463 fh = os.fdopen(fd, r"wb")
463 464 d = fp.read(4096)
464 465 while d:
465 466 fh.write(d)
466 467 d = fp.read(4096)
467 468 fh.close()
468 469 # start http push
469 470 fp_ = httpconnection.httpsendfile(self.ui, filename, "rb")
470 471 headers = {r'Content-Type': r'application/mercurial-0.1'}
471 472 return self._callstream(cmd, data=fp_, headers=headers, **args)
472 473 finally:
473 474 if fp_ is not None:
474 475 fp_.close()
475 476 if fh is not None:
476 477 fh.close()
477 478 os.unlink(filename)
478 479
479 480 def _callcompressable(self, cmd, **args):
480 481 return self._callstream(cmd, _compressible=True, **args)
481 482
482 483 def _abort(self, exception):
483 484 raise exception
484 485
485 486 def instance(ui, path, create):
486 487 if create:
487 488 raise error.Abort(_('cannot create new http repository'))
488 489 try:
489 490 if path.startswith('https:') and not urlmod.has_https:
490 491 raise error.Abort(_('Python support for SSL and HTTPS '
491 492 'is not installed'))
492 493
493 494 inst = httppeer(ui, path)
494 495 inst._fetchcaps()
495 496
496 497 return inst
497 498 except error.RepoError as httpexception:
498 499 try:
499 500 r = statichttprepo.instance(ui, "static-" + path, create)
500 501 ui.note(_('(falling back to static-http)\n'))
501 502 return r
502 503 except error.RepoError:
503 504 raise httpexception # use the original http RepoError instead
General Comments 0
You need to be logged in to leave comments. Login now