##// END OF EJS Templates
httppeer: implement ipeerconnection...
Gregory Szorc -
r37627:01bfe5ad default
parent child Browse files
Show More
@@ -1,767 +1,789 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 .thirdparty import (
20 20 cbor,
21 21 )
22 from .thirdparty.zope import (
23 interface as zi,
24 )
22 25 from . import (
23 26 bundle2,
24 27 error,
25 28 httpconnection,
26 29 pycompat,
30 repository,
27 31 statichttprepo,
28 32 url as urlmod,
29 33 util,
30 34 wireproto,
31 35 wireprotoframing,
32 36 wireprototypes,
33 37 wireprotov2server,
34 38 )
35 39
36 40 httplib = util.httplib
37 41 urlerr = util.urlerr
38 42 urlreq = util.urlreq
39 43
40 44 def encodevalueinheaders(value, header, limit):
41 45 """Encode a string value into multiple HTTP headers.
42 46
43 47 ``value`` will be encoded into 1 or more HTTP headers with the names
44 48 ``header-<N>`` where ``<N>`` is an integer starting at 1. Each header
45 49 name + value will be at most ``limit`` bytes long.
46 50
47 51 Returns an iterable of 2-tuples consisting of header names and
48 52 values as native strings.
49 53 """
50 54 # HTTP Headers are ASCII. Python 3 requires them to be unicodes,
51 55 # not bytes. This function always takes bytes in as arguments.
52 56 fmt = pycompat.strurl(header) + r'-%s'
53 57 # Note: it is *NOT* a bug that the last bit here is a bytestring
54 58 # and not a unicode: we're just getting the encoded length anyway,
55 59 # and using an r-string to make it portable between Python 2 and 3
56 60 # doesn't work because then the \r is a literal backslash-r
57 61 # instead of a carriage return.
58 62 valuelen = limit - len(fmt % r'000') - len(': \r\n')
59 63 result = []
60 64
61 65 n = 0
62 66 for i in xrange(0, len(value), valuelen):
63 67 n += 1
64 68 result.append((fmt % str(n), pycompat.strurl(value[i:i + valuelen])))
65 69
66 70 return result
67 71
68 72 def _wraphttpresponse(resp):
69 73 """Wrap an HTTPResponse with common error handlers.
70 74
71 75 This ensures that any I/O from any consumer raises the appropriate
72 76 error and messaging.
73 77 """
74 78 origread = resp.read
75 79
76 80 class readerproxy(resp.__class__):
77 81 def read(self, size=None):
78 82 try:
79 83 return origread(size)
80 84 except httplib.IncompleteRead as e:
81 85 # e.expected is an integer if length known or None otherwise.
82 86 if e.expected:
83 87 msg = _('HTTP request error (incomplete response; '
84 88 'expected %d bytes got %d)') % (e.expected,
85 89 len(e.partial))
86 90 else:
87 91 msg = _('HTTP request error (incomplete response)')
88 92
89 93 raise error.PeerTransportError(
90 94 msg,
91 95 hint=_('this may be an intermittent network failure; '
92 96 'if the error persists, consider contacting the '
93 97 'network or server operator'))
94 98 except httplib.HTTPException as e:
95 99 raise error.PeerTransportError(
96 100 _('HTTP request error (%s)') % e,
97 101 hint=_('this may be an intermittent network failure; '
98 102 'if the error persists, consider contacting the '
99 103 'network or server operator'))
100 104
101 105 resp.__class__ = readerproxy
102 106
103 107 class _multifile(object):
104 108 def __init__(self, *fileobjs):
105 109 for f in fileobjs:
106 110 if not util.safehasattr(f, 'length'):
107 111 raise ValueError(
108 112 '_multifile only supports file objects that '
109 113 'have a length but this one does not:', type(f), f)
110 114 self._fileobjs = fileobjs
111 115 self._index = 0
112 116
113 117 @property
114 118 def length(self):
115 119 return sum(f.length for f in self._fileobjs)
116 120
117 121 def read(self, amt=None):
118 122 if amt <= 0:
119 123 return ''.join(f.read() for f in self._fileobjs)
120 124 parts = []
121 125 while amt and self._index < len(self._fileobjs):
122 126 parts.append(self._fileobjs[self._index].read(amt))
123 127 got = len(parts[-1])
124 128 if got < amt:
125 129 self._index += 1
126 130 amt -= got
127 131 return ''.join(parts)
128 132
129 133 def seek(self, offset, whence=os.SEEK_SET):
130 134 if whence != os.SEEK_SET:
131 135 raise NotImplementedError(
132 136 '_multifile does not support anything other'
133 137 ' than os.SEEK_SET for whence on seek()')
134 138 if offset != 0:
135 139 raise NotImplementedError(
136 140 '_multifile only supports seeking to start, but that '
137 141 'could be fixed if you need it')
138 142 for f in self._fileobjs:
139 143 f.seek(0)
140 144 self._index = 0
141 145
142 146 def makev1commandrequest(ui, requestbuilder, caps, capablefn,
143 147 repobaseurl, cmd, args):
144 148 """Make an HTTP request to run a command for a version 1 client.
145 149
146 150 ``caps`` is a set of known server capabilities. The value may be
147 151 None if capabilities are not yet known.
148 152
149 153 ``capablefn`` is a function to evaluate a capability.
150 154
151 155 ``cmd``, ``args``, and ``data`` define the command, its arguments, and
152 156 raw data to pass to it.
153 157 """
154 158 if cmd == 'pushkey':
155 159 args['data'] = ''
156 160 data = args.pop('data', None)
157 161 headers = args.pop('headers', {})
158 162
159 163 ui.debug("sending %s command\n" % cmd)
160 164 q = [('cmd', cmd)]
161 165 headersize = 0
162 166 # Important: don't use self.capable() here or else you end up
163 167 # with infinite recursion when trying to look up capabilities
164 168 # for the first time.
165 169 postargsok = caps is not None and 'httppostargs' in caps
166 170
167 171 # Send arguments via POST.
168 172 if postargsok and args:
169 173 strargs = urlreq.urlencode(sorted(args.items()))
170 174 if not data:
171 175 data = strargs
172 176 else:
173 177 if isinstance(data, bytes):
174 178 i = io.BytesIO(data)
175 179 i.length = len(data)
176 180 data = i
177 181 argsio = io.BytesIO(strargs)
178 182 argsio.length = len(strargs)
179 183 data = _multifile(argsio, data)
180 184 headers[r'X-HgArgs-Post'] = len(strargs)
181 185 elif args:
182 186 # Calling self.capable() can infinite loop if we are calling
183 187 # "capabilities". But that command should never accept wire
184 188 # protocol arguments. So this should never happen.
185 189 assert cmd != 'capabilities'
186 190 httpheader = capablefn('httpheader')
187 191 if httpheader:
188 192 headersize = int(httpheader.split(',', 1)[0])
189 193
190 194 # Send arguments via HTTP headers.
191 195 if headersize > 0:
192 196 # The headers can typically carry more data than the URL.
193 197 encargs = urlreq.urlencode(sorted(args.items()))
194 198 for header, value in encodevalueinheaders(encargs, 'X-HgArg',
195 199 headersize):
196 200 headers[header] = value
197 201 # Send arguments via query string (Mercurial <1.9).
198 202 else:
199 203 q += sorted(args.items())
200 204
201 205 qs = '?%s' % urlreq.urlencode(q)
202 206 cu = "%s%s" % (repobaseurl, qs)
203 207 size = 0
204 208 if util.safehasattr(data, 'length'):
205 209 size = data.length
206 210 elif data is not None:
207 211 size = len(data)
208 212 if data is not None and r'Content-Type' not in headers:
209 213 headers[r'Content-Type'] = r'application/mercurial-0.1'
210 214
211 215 # Tell the server we accept application/mercurial-0.2 and multiple
212 216 # compression formats if the server is capable of emitting those
213 217 # payloads.
214 218 # Note: Keep this set empty by default, as client advertisement of
215 219 # protocol parameters should only occur after the handshake.
216 220 protoparams = set()
217 221
218 222 mediatypes = set()
219 223 if caps is not None:
220 224 mt = capablefn('httpmediatype')
221 225 if mt:
222 226 protoparams.add('0.1')
223 227 mediatypes = set(mt.split(','))
224 228
225 229 protoparams.add('partial-pull')
226 230
227 231 if '0.2tx' in mediatypes:
228 232 protoparams.add('0.2')
229 233
230 234 if '0.2tx' in mediatypes and capablefn('compression'):
231 235 # We /could/ compare supported compression formats and prune
232 236 # non-mutually supported or error if nothing is mutually supported.
233 237 # For now, send the full list to the server and have it error.
234 238 comps = [e.wireprotosupport().name for e in
235 239 util.compengines.supportedwireengines(util.CLIENTROLE)]
236 240 protoparams.add('comp=%s' % ','.join(comps))
237 241
238 242 if protoparams:
239 243 protoheaders = encodevalueinheaders(' '.join(sorted(protoparams)),
240 244 'X-HgProto',
241 245 headersize or 1024)
242 246 for header, value in protoheaders:
243 247 headers[header] = value
244 248
245 249 varyheaders = []
246 250 for header in headers:
247 251 if header.lower().startswith(r'x-hg'):
248 252 varyheaders.append(header)
249 253
250 254 if varyheaders:
251 255 headers[r'Vary'] = r','.join(sorted(varyheaders))
252 256
253 257 req = requestbuilder(pycompat.strurl(cu), data, headers)
254 258
255 259 if data is not None:
256 260 ui.debug("sending %d bytes\n" % size)
257 261 req.add_unredirected_header(r'Content-Length', r'%d' % size)
258 262
259 263 return req, cu, qs
260 264
261 265 def sendrequest(ui, opener, req):
262 266 """Send a prepared HTTP request.
263 267
264 268 Returns the response object.
265 269 """
266 270 if (ui.debugflag
267 271 and ui.configbool('devel', 'debug.peer-request')):
268 272 dbg = ui.debug
269 273 line = 'devel-peer-request: %s\n'
270 274 dbg(line % '%s %s' % (req.get_method(), req.get_full_url()))
271 275 hgargssize = None
272 276
273 277 for header, value in sorted(req.header_items()):
274 278 if header.startswith('X-hgarg-'):
275 279 if hgargssize is None:
276 280 hgargssize = 0
277 281 hgargssize += len(value)
278 282 else:
279 283 dbg(line % ' %s %s' % (header, value))
280 284
281 285 if hgargssize is not None:
282 286 dbg(line % ' %d bytes of commands arguments in headers'
283 287 % hgargssize)
284 288
285 289 if req.has_data():
286 290 data = req.get_data()
287 291 length = getattr(data, 'length', None)
288 292 if length is None:
289 293 length = len(data)
290 294 dbg(line % ' %d bytes of data' % length)
291 295
292 296 start = util.timer()
293 297
294 298 try:
295 299 res = opener.open(req)
296 300 except urlerr.httperror as inst:
297 301 if inst.code == 401:
298 302 raise error.Abort(_('authorization failed'))
299 303 raise
300 304 except httplib.HTTPException as inst:
301 305 ui.debug('http error requesting %s\n' %
302 306 util.hidepassword(req.get_full_url()))
303 307 ui.traceback()
304 308 raise IOError(None, inst)
305 309 finally:
306 310 if ui.configbool('devel', 'debug.peer-request'):
307 311 dbg(line % ' finished in %.4f seconds (%s)'
308 312 % (util.timer() - start, res.code))
309 313
310 314 # Insert error handlers for common I/O failures.
311 315 _wraphttpresponse(res)
312 316
313 317 return res
314 318
315 319 def parsev1commandresponse(ui, baseurl, requrl, qs, resp, compressible,
316 320 allowcbor=False):
317 321 # record the url we got redirected to
318 322 respurl = pycompat.bytesurl(resp.geturl())
319 323 if respurl.endswith(qs):
320 324 respurl = respurl[:-len(qs)]
321 325 if baseurl.rstrip('/') != respurl.rstrip('/'):
322 326 if not ui.quiet:
323 327 ui.warn(_('real URL is %s\n') % respurl)
324 328
325 329 try:
326 330 proto = pycompat.bytesurl(resp.getheader(r'content-type', r''))
327 331 except AttributeError:
328 332 proto = pycompat.bytesurl(resp.headers.get(r'content-type', r''))
329 333
330 334 safeurl = util.hidepassword(baseurl)
331 335 if proto.startswith('application/hg-error'):
332 336 raise error.OutOfBandError(resp.read())
333 337
334 338 # Pre 1.0 versions of Mercurial used text/plain and
335 339 # application/hg-changegroup. We don't support such old servers.
336 340 if not proto.startswith('application/mercurial-'):
337 341 ui.debug("requested URL: '%s'\n" % util.hidepassword(requrl))
338 342 raise error.RepoError(
339 343 _("'%s' does not appear to be an hg repository:\n"
340 344 "---%%<--- (%s)\n%s\n---%%<---\n")
341 345 % (safeurl, proto or 'no content-type', resp.read(1024)))
342 346
343 347 try:
344 348 subtype = proto.split('-', 1)[1]
345 349
346 350 # Unless we end up supporting CBOR in the legacy wire protocol,
347 351 # this should ONLY be encountered for the initial capabilities
348 352 # request during handshake.
349 353 if subtype == 'cbor':
350 354 if allowcbor:
351 355 return respurl, proto, resp
352 356 else:
353 357 raise error.RepoError(_('unexpected CBOR response from '
354 358 'server'))
355 359
356 360 version_info = tuple([int(n) for n in subtype.split('.')])
357 361 except ValueError:
358 362 raise error.RepoError(_("'%s' sent a broken Content-Type "
359 363 "header (%s)") % (safeurl, proto))
360 364
361 365 # TODO consider switching to a decompression reader that uses
362 366 # generators.
363 367 if version_info == (0, 1):
364 368 if compressible:
365 369 resp = util.compengines['zlib'].decompressorreader(resp)
366 370
367 371 elif version_info == (0, 2):
368 372 # application/mercurial-0.2 always identifies the compression
369 373 # engine in the payload header.
370 374 elen = struct.unpack('B', resp.read(1))[0]
371 375 ename = resp.read(elen)
372 376 engine = util.compengines.forwiretype(ename)
373 377
374 378 resp = engine.decompressorreader(resp)
375 379 else:
376 380 raise error.RepoError(_("'%s' uses newer protocol %s") %
377 381 (safeurl, subtype))
378 382
379 383 return respurl, proto, resp
380 384
381 385 class httppeer(wireproto.wirepeer):
382 386 def __init__(self, ui, path, url, opener, requestbuilder, caps):
383 387 self.ui = ui
384 388 self._path = path
385 389 self._url = url
386 390 self._caps = caps
387 391 self._urlopener = opener
388 392 self._requestbuilder = requestbuilder
389 393
390 394 def __del__(self):
391 395 for h in self._urlopener.handlers:
392 396 h.close()
393 397 getattr(h, "close_all", lambda: None)()
394 398
395 399 # Begin of ipeerconnection interface.
396 400
397 401 def url(self):
398 402 return self._path
399 403
400 404 def local(self):
401 405 return None
402 406
403 407 def peer(self):
404 408 return self
405 409
406 410 def canpush(self):
407 411 return True
408 412
409 413 def close(self):
410 414 pass
411 415
412 416 # End of ipeerconnection interface.
413 417
414 418 # Begin of ipeercommands interface.
415 419
416 420 def capabilities(self):
417 421 return self._caps
418 422
419 423 # End of ipeercommands interface.
420 424
421 425 # look up capabilities only when needed
422 426
423 427 def _callstream(self, cmd, _compressible=False, **args):
424 428 args = pycompat.byteskwargs(args)
425 429
426 430 req, cu, qs = makev1commandrequest(self.ui, self._requestbuilder,
427 431 self._caps, self.capable,
428 432 self._url, cmd, args)
429 433
430 434 resp = sendrequest(self.ui, self._urlopener, req)
431 435
432 436 self._url, ct, resp = parsev1commandresponse(self.ui, self._url, cu, qs,
433 437 resp, _compressible)
434 438
435 439 return resp
436 440
437 441 def _call(self, cmd, **args):
438 442 fp = self._callstream(cmd, **args)
439 443 try:
440 444 return fp.read()
441 445 finally:
442 446 # if using keepalive, allow connection to be reused
443 447 fp.close()
444 448
445 449 def _callpush(self, cmd, cg, **args):
446 450 # have to stream bundle to a temp file because we do not have
447 451 # http 1.1 chunked transfer.
448 452
449 453 types = self.capable('unbundle')
450 454 try:
451 455 types = types.split(',')
452 456 except AttributeError:
453 457 # servers older than d1b16a746db6 will send 'unbundle' as a
454 458 # boolean capability. They only support headerless/uncompressed
455 459 # bundles.
456 460 types = [""]
457 461 for x in types:
458 462 if x in bundle2.bundletypes:
459 463 type = x
460 464 break
461 465
462 466 tempname = bundle2.writebundle(self.ui, cg, None, type)
463 467 fp = httpconnection.httpsendfile(self.ui, tempname, "rb")
464 468 headers = {r'Content-Type': r'application/mercurial-0.1'}
465 469
466 470 try:
467 471 r = self._call(cmd, data=fp, headers=headers, **args)
468 472 vals = r.split('\n', 1)
469 473 if len(vals) < 2:
470 474 raise error.ResponseError(_("unexpected response:"), r)
471 475 return vals
472 476 except urlerr.httperror:
473 477 # Catch and re-raise these so we don't try and treat them
474 478 # like generic socket errors. They lack any values in
475 479 # .args on Python 3 which breaks our socket.error block.
476 480 raise
477 481 except socket.error as err:
478 482 if err.args[0] in (errno.ECONNRESET, errno.EPIPE):
479 483 raise error.Abort(_('push failed: %s') % err.args[1])
480 484 raise error.Abort(err.args[1])
481 485 finally:
482 486 fp.close()
483 487 os.unlink(tempname)
484 488
485 489 def _calltwowaystream(self, cmd, fp, **args):
486 490 fh = None
487 491 fp_ = None
488 492 filename = None
489 493 try:
490 494 # dump bundle to disk
491 495 fd, filename = tempfile.mkstemp(prefix="hg-bundle-", suffix=".hg")
492 496 fh = os.fdopen(fd, r"wb")
493 497 d = fp.read(4096)
494 498 while d:
495 499 fh.write(d)
496 500 d = fp.read(4096)
497 501 fh.close()
498 502 # start http push
499 503 fp_ = httpconnection.httpsendfile(self.ui, filename, "rb")
500 504 headers = {r'Content-Type': r'application/mercurial-0.1'}
501 505 return self._callstream(cmd, data=fp_, headers=headers, **args)
502 506 finally:
503 507 if fp_ is not None:
504 508 fp_.close()
505 509 if fh is not None:
506 510 fh.close()
507 511 os.unlink(filename)
508 512
509 513 def _callcompressable(self, cmd, **args):
510 514 return self._callstream(cmd, _compressible=True, **args)
511 515
512 516 def _abort(self, exception):
513 517 raise exception
514 518
515 519 # TODO implement interface for version 2 peers
520 @zi.implementer(repository.ipeerconnection)
516 521 class httpv2peer(object):
517 522 def __init__(self, ui, repourl, apipath, opener, requestbuilder,
518 523 apidescriptor):
519 524 self.ui = ui
520 525
521 526 if repourl.endswith('/'):
522 527 repourl = repourl[:-1]
523 528
524 self.url = repourl
529 self._url = repourl
525 530 self._apipath = apipath
526 531 self._opener = opener
527 532 self._requestbuilder = requestbuilder
528 533 self._descriptor = apidescriptor
529 534
535 # Start of ipeerconnection.
536
537 def url(self):
538 return self._url
539
540 def local(self):
541 return None
542
543 def peer(self):
544 return self
545
546 def canpush(self):
547 # TODO change once implemented.
548 return False
549
530 550 def close(self):
531 551 pass
532 552
553 # End of ipeerconnection.
554
533 555 # TODO require to be part of a batched primitive, use futures.
534 556 def _call(self, name, **args):
535 557 """Call a wire protocol command with arguments."""
536 558
537 559 # Having this early has a side-effect of importing wireprotov2server,
538 560 # which has the side-effect of ensuring commands are registered.
539 561
540 562 # TODO modify user-agent to reflect v2.
541 563 headers = {
542 564 r'Accept': wireprotov2server.FRAMINGTYPE,
543 565 r'Content-Type': wireprotov2server.FRAMINGTYPE,
544 566 }
545 567
546 568 # TODO permissions should come from capabilities results.
547 569 permission = wireproto.commandsv2[name].permission
548 570 if permission not in ('push', 'pull'):
549 571 raise error.ProgrammingError('unknown permission type: %s' %
550 572 permission)
551 573
552 574 permission = {
553 575 'push': 'rw',
554 576 'pull': 'ro',
555 577 }[permission]
556 578
557 url = '%s/%s/%s/%s' % (self.url, self._apipath, permission, name)
579 url = '%s/%s/%s/%s' % (self._url, self._apipath, permission, name)
558 580
559 581 # TODO this should be part of a generic peer for the frame-based
560 582 # protocol.
561 583 reactor = wireprotoframing.clientreactor(hasmultiplesend=False,
562 584 buffersends=True)
563 585
564 586 request, action, meta = reactor.callcommand(name, args)
565 587 assert action == 'noop'
566 588
567 589 action, meta = reactor.flushcommands()
568 590 assert action == 'sendframes'
569 591
570 592 body = b''.join(map(bytes, meta['framegen']))
571 593 req = self._requestbuilder(pycompat.strurl(url), body, headers)
572 594 req.add_unredirected_header(r'Content-Length', r'%d' % len(body))
573 595
574 596 # TODO unify this code with httppeer.
575 597 try:
576 598 res = self._opener.open(req)
577 599 except urlerr.httperror as e:
578 600 if e.code == 401:
579 601 raise error.Abort(_('authorization failed'))
580 602
581 603 raise
582 604 except httplib.HTTPException as e:
583 605 self.ui.traceback()
584 606 raise IOError(None, e)
585 607
586 608 # TODO validate response type, wrap response to handle I/O errors.
587 609 # TODO more robust frame receiver.
588 610 results = []
589 611
590 612 while True:
591 613 frame = wireprotoframing.readframe(res)
592 614 if frame is None:
593 615 break
594 616
595 617 self.ui.note(_('received %r\n') % frame)
596 618
597 619 action, meta = reactor.onframerecv(frame)
598 620
599 621 if action == 'responsedata':
600 622 if meta['cbor']:
601 623 payload = util.bytesio(meta['data'])
602 624
603 625 decoder = cbor.CBORDecoder(payload)
604 626 while payload.tell() + 1 < len(meta['data']):
605 627 results.append(decoder.decode())
606 628 else:
607 629 results.append(meta['data'])
608 630 else:
609 631 error.ProgrammingError('unhandled action: %s' % action)
610 632
611 633 return results
612 634
613 635 # Registry of API service names to metadata about peers that handle it.
614 636 #
615 637 # The following keys are meaningful:
616 638 #
617 639 # init
618 640 # Callable receiving (ui, repourl, servicepath, opener, requestbuilder,
619 641 # apidescriptor) to create a peer.
620 642 #
621 643 # priority
622 644 # Integer priority for the service. If we could choose from multiple
623 645 # services, we choose the one with the highest priority.
624 646 API_PEERS = {
625 647 wireprototypes.HTTPV2: {
626 648 'init': httpv2peer,
627 649 'priority': 50,
628 650 },
629 651 }
630 652
631 653 def performhandshake(ui, url, opener, requestbuilder):
632 654 # The handshake is a request to the capabilities command.
633 655
634 656 caps = None
635 657 def capable(x):
636 658 raise error.ProgrammingError('should not be called')
637 659
638 660 args = {}
639 661
640 662 # The client advertises support for newer protocols by adding an
641 663 # X-HgUpgrade-* header with a list of supported APIs and an
642 664 # X-HgProto-* header advertising which serializing formats it supports.
643 665 # We only support the HTTP version 2 transport and CBOR responses for
644 666 # now.
645 667 advertisev2 = ui.configbool('experimental', 'httppeer.advertise-v2')
646 668
647 669 if advertisev2:
648 670 args['headers'] = {
649 671 r'X-HgProto-1': r'cbor',
650 672 }
651 673
652 674 args['headers'].update(
653 675 encodevalueinheaders(' '.join(sorted(API_PEERS)),
654 676 'X-HgUpgrade',
655 677 # We don't know the header limit this early.
656 678 # So make it small.
657 679 1024))
658 680
659 681 req, requrl, qs = makev1commandrequest(ui, requestbuilder, caps,
660 682 capable, url, 'capabilities',
661 683 args)
662 684
663 685 resp = sendrequest(ui, opener, req)
664 686
665 687 respurl, ct, resp = parsev1commandresponse(ui, url, requrl, qs, resp,
666 688 compressible=False,
667 689 allowcbor=advertisev2)
668 690
669 691 try:
670 692 rawdata = resp.read()
671 693 finally:
672 694 resp.close()
673 695
674 696 if not ct.startswith('application/mercurial-'):
675 697 raise error.ProgrammingError('unexpected content-type: %s' % ct)
676 698
677 699 if advertisev2:
678 700 if ct == 'application/mercurial-cbor':
679 701 try:
680 702 info = cbor.loads(rawdata)
681 703 except cbor.CBORDecodeError:
682 704 raise error.Abort(_('error decoding CBOR from remote server'),
683 705 hint=_('try again and consider contacting '
684 706 'the server operator'))
685 707
686 708 # We got a legacy response. That's fine.
687 709 elif ct in ('application/mercurial-0.1', 'application/mercurial-0.2'):
688 710 info = {
689 711 'v1capabilities': set(rawdata.split())
690 712 }
691 713
692 714 else:
693 715 raise error.RepoError(
694 716 _('unexpected response type from server: %s') % ct)
695 717 else:
696 718 info = {
697 719 'v1capabilities': set(rawdata.split())
698 720 }
699 721
700 722 return respurl, info
701 723
702 724 def makepeer(ui, path, opener=None, requestbuilder=urlreq.request):
703 725 """Construct an appropriate HTTP peer instance.
704 726
705 727 ``opener`` is an ``url.opener`` that should be used to establish
706 728 connections, perform HTTP requests.
707 729
708 730 ``requestbuilder`` is the type used for constructing HTTP requests.
709 731 It exists as an argument so extensions can override the default.
710 732 """
711 733 u = util.url(path)
712 734 if u.query or u.fragment:
713 735 raise error.Abort(_('unsupported URL component: "%s"') %
714 736 (u.query or u.fragment))
715 737
716 738 # urllib cannot handle URLs with embedded user or passwd.
717 739 url, authinfo = u.authinfo()
718 740 ui.debug('using %s\n' % url)
719 741
720 742 opener = opener or urlmod.opener(ui, authinfo)
721 743
722 744 respurl, info = performhandshake(ui, url, opener, requestbuilder)
723 745
724 746 # Given the intersection of APIs that both we and the server support,
725 747 # sort by their advertised priority and pick the first one.
726 748 #
727 749 # TODO consider making this request-based and interface driven. For
728 750 # example, the caller could say "I want a peer that does X." It's quite
729 751 # possible that not all peers would do that. Since we know the service
730 752 # capabilities, we could filter out services not meeting the
731 753 # requirements. Possibly by consulting the interfaces defined by the
732 754 # peer type.
733 755 apipeerchoices = set(info.get('apis', {}).keys()) & set(API_PEERS.keys())
734 756
735 757 preferredchoices = sorted(apipeerchoices,
736 758 key=lambda x: API_PEERS[x]['priority'],
737 759 reverse=True)
738 760
739 761 for service in preferredchoices:
740 762 apipath = '%s/%s' % (info['apibase'].rstrip('/'), service)
741 763
742 764 return API_PEERS[service]['init'](ui, respurl, apipath, opener,
743 765 requestbuilder,
744 766 info['apis'][service])
745 767
746 768 # Failed to construct an API peer. Fall back to legacy.
747 769 return httppeer(ui, path, respurl, opener, requestbuilder,
748 770 info['v1capabilities'])
749 771
750 772 def instance(ui, path, create):
751 773 if create:
752 774 raise error.Abort(_('cannot create new http repository'))
753 775 try:
754 776 if path.startswith('https:') and not urlmod.has_https:
755 777 raise error.Abort(_('Python support for SSL and HTTPS '
756 778 'is not installed'))
757 779
758 780 inst = makepeer(ui, path)
759 781
760 782 return inst
761 783 except error.RepoError as httpexception:
762 784 try:
763 785 r = statichttprepo.instance(ui, "static-" + path, create)
764 786 ui.note(_('(falling back to static-http)\n'))
765 787 return r
766 788 except error.RepoError:
767 789 raise httpexception # use the original http RepoError instead
@@ -1,148 +1,152 b''
1 1 # Test that certain objects conform to well-defined interfaces.
2 2
3 3 from __future__ import absolute_import, print_function
4 4
5 5 import os
6 6
7 7 from mercurial.thirdparty.zope import (
8 8 interface as zi,
9 9 )
10 10 from mercurial.thirdparty.zope.interface import (
11 11 verify as ziverify,
12 12 )
13 13 from mercurial import (
14 14 bundlerepo,
15 15 filelog,
16 16 httppeer,
17 17 localrepo,
18 18 repository,
19 19 sshpeer,
20 20 statichttprepo,
21 21 ui as uimod,
22 22 unionrepo,
23 23 vfs as vfsmod,
24 24 wireprotoserver,
25 25 wireprototypes,
26 26 wireprotov2server,
27 27 )
28 28
29 29 rootdir = os.path.normpath(os.path.join(os.path.dirname(__file__), '..'))
30 30
31 31 def checkzobject(o, allowextra=False):
32 32 """Verify an object with a zope interface."""
33 33 ifaces = zi.providedBy(o)
34 34 if not ifaces:
35 35 print('%r does not provide any zope interfaces' % o)
36 36 return
37 37
38 38 # Run zope.interface's built-in verification routine. This verifies that
39 39 # everything that is supposed to be present is present.
40 40 for iface in ifaces:
41 41 ziverify.verifyObject(iface, o)
42 42
43 43 if allowextra:
44 44 return
45 45
46 46 # Now verify that the object provides no extra public attributes that
47 47 # aren't declared as part of interfaces.
48 48 allowed = set()
49 49 for iface in ifaces:
50 50 allowed |= set(iface.names(all=True))
51 51
52 52 public = {a for a in dir(o) if not a.startswith('_')}
53 53
54 54 for attr in sorted(public - allowed):
55 55 print('public attribute not declared in interfaces: %s.%s' % (
56 56 o.__class__.__name__, attr))
57 57
58 58 # Facilitates testing localpeer.
59 59 class dummyrepo(object):
60 60 def __init__(self):
61 61 self.ui = uimod.ui()
62 62 def filtered(self, name):
63 63 pass
64 64 def _restrictcapabilities(self, caps):
65 65 pass
66 66
67 67 class dummyopener(object):
68 68 handlers = []
69 69
70 70 # Facilitates testing sshpeer without requiring a server.
71 71 class badpeer(httppeer.httppeer):
72 72 def __init__(self):
73 73 super(badpeer, self).__init__(None, None, None, dummyopener(), None,
74 74 None)
75 75 self.badattribute = True
76 76
77 77 def badmethod(self):
78 78 pass
79 79
80 80 class dummypipe(object):
81 81 def close(self):
82 82 pass
83 83
84 84 def main():
85 85 ui = uimod.ui()
86 86 # Needed so we can open a local repo with obsstore without a warning.
87 87 ui.setconfig('experimental', 'evolution.createmarkers', True)
88 88
89 89 checkzobject(badpeer())
90 90
91 91 ziverify.verifyClass(repository.ipeerbaselegacycommands,
92 92 httppeer.httppeer)
93 93 checkzobject(httppeer.httppeer(None, None, None, dummyopener(), None, None))
94 94
95 ziverify.verifyClass(repository.ipeerconnection,
96 httppeer.httpv2peer)
97 checkzobject(httppeer.httpv2peer(None, '', None, None, None, None))
98
95 99 ziverify.verifyClass(repository.ipeerbase,
96 100 localrepo.localpeer)
97 101 checkzobject(localrepo.localpeer(dummyrepo()))
98 102
99 103 ziverify.verifyClass(repository.ipeerbaselegacycommands,
100 104 sshpeer.sshv1peer)
101 105 checkzobject(sshpeer.sshv1peer(ui, 'ssh://localhost/foo', None, dummypipe(),
102 106 dummypipe(), None, None))
103 107
104 108 ziverify.verifyClass(repository.ipeerbaselegacycommands,
105 109 sshpeer.sshv2peer)
106 110 checkzobject(sshpeer.sshv2peer(ui, 'ssh://localhost/foo', None, dummypipe(),
107 111 dummypipe(), None, None))
108 112
109 113 ziverify.verifyClass(repository.ipeerbase, bundlerepo.bundlepeer)
110 114 checkzobject(bundlerepo.bundlepeer(dummyrepo()))
111 115
112 116 ziverify.verifyClass(repository.ipeerbase, statichttprepo.statichttppeer)
113 117 checkzobject(statichttprepo.statichttppeer(dummyrepo()))
114 118
115 119 ziverify.verifyClass(repository.ipeerbase, unionrepo.unionpeer)
116 120 checkzobject(unionrepo.unionpeer(dummyrepo()))
117 121
118 122 ziverify.verifyClass(repository.completelocalrepository,
119 123 localrepo.localrepository)
120 124 repo = localrepo.localrepository(ui, rootdir)
121 125 checkzobject(repo)
122 126
123 127 ziverify.verifyClass(wireprototypes.baseprotocolhandler,
124 128 wireprotoserver.sshv1protocolhandler)
125 129 ziverify.verifyClass(wireprototypes.baseprotocolhandler,
126 130 wireprotoserver.sshv2protocolhandler)
127 131 ziverify.verifyClass(wireprototypes.baseprotocolhandler,
128 132 wireprotoserver.httpv1protocolhandler)
129 133 ziverify.verifyClass(wireprototypes.baseprotocolhandler,
130 134 wireprotov2server.httpv2protocolhandler)
131 135
132 136 sshv1 = wireprotoserver.sshv1protocolhandler(None, None, None)
133 137 checkzobject(sshv1)
134 138 sshv2 = wireprotoserver.sshv2protocolhandler(None, None, None)
135 139 checkzobject(sshv2)
136 140
137 141 httpv1 = wireprotoserver.httpv1protocolhandler(None, None, None)
138 142 checkzobject(httpv1)
139 143 httpv2 = wireprotov2server.httpv2protocolhandler(None, None)
140 144 checkzobject(httpv2)
141 145
142 146 ziverify.verifyClass(repository.ifilestorage, filelog.filelog)
143 147
144 148 vfs = vfsmod.vfs('.')
145 149 fl = filelog.filelog(vfs, 'dummy.i')
146 150 checkzobject(fl, allowextra=True)
147 151
148 152 main()
General Comments 0
You need to be logged in to leave comments. Login now