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