comparison mercurial/wireprotoserver.py @ 48561:04688c51f81f

exchangev2: remove it As discussed on the mailing list, this is incomplete and unused with little hope of revival. Differential Revision: https://phab.mercurial-scm.org/D11954
author Rapha?l Gom?s <rgomes@octobus.net>
date Tue, 07 Dec 2021 16:44:22 +0100
parents 81805bba11f9
children 6000f5b25c9b
comparison
equal deleted inserted replaced
48560:d6c53b40b078 48561:04688c51f81f
16 error, 16 error,
17 pycompat, 17 pycompat,
18 util, 18 util,
19 wireprototypes, 19 wireprototypes,
20 wireprotov1server, 20 wireprotov1server,
21 wireprotov2server,
22 ) 21 )
23 from .interfaces import util as interfaceutil 22 from .interfaces import util as interfaceutil
24 from .utils import ( 23 from .utils import (
25 cborutil,
26 compression, 24 compression,
27 stringutil, 25 stringutil,
28 ) 26 )
29 27
30 stringio = util.stringio 28 stringio = util.stringio
37 HGTYPE = b'application/mercurial-0.1' 35 HGTYPE = b'application/mercurial-0.1'
38 HGTYPE2 = b'application/mercurial-0.2' 36 HGTYPE2 = b'application/mercurial-0.2'
39 HGERRTYPE = b'application/hg-error' 37 HGERRTYPE = b'application/hg-error'
40 38
41 SSHV1 = wireprototypes.SSHV1 39 SSHV1 = wireprototypes.SSHV1
42 SSHV2 = wireprototypes.SSHV2
43 40
44 41
45 def decodevaluefromheaders(req, headerprefix): 42 def decodevaluefromheaders(req, headerprefix):
46 """Decode a long value from multiple HTTP request headers. 43 """Decode a long value from multiple HTTP request headers.
47 44
242 res.setbodybytes(b'0\n%s\n' % stringutil.forcebytestr(e)) 239 res.setbodybytes(b'0\n%s\n' % stringutil.forcebytestr(e))
243 240
244 return True 241 return True
245 242
246 243
247 def _availableapis(repo):
248 apis = set()
249
250 # Registered APIs are made available via config options of the name of
251 # the protocol.
252 for k, v in API_HANDLERS.items():
253 section, option = v[b'config'] # pytype: disable=attribute-error
254 if repo.ui.configbool(section, option):
255 apis.add(k)
256
257 return apis
258
259
260 def handlewsgiapirequest(rctx, req, res, checkperm):
261 """Handle requests to /api/*."""
262 assert req.dispatchparts[0] == b'api'
263
264 repo = rctx.repo
265
266 # This whole URL space is experimental for now. But we want to
267 # reserve the URL space. So, 404 all URLs if the feature isn't enabled.
268 if not repo.ui.configbool(b'experimental', b'web.apiserver'):
269 res.status = b'404 Not Found'
270 res.headers[b'Content-Type'] = b'text/plain'
271 res.setbodybytes(_(b'Experimental API server endpoint not enabled'))
272 return
273
274 # The URL space is /api/<protocol>/*. The structure of URLs under varies
275 # by <protocol>.
276
277 availableapis = _availableapis(repo)
278
279 # Requests to /api/ list available APIs.
280 if req.dispatchparts == [b'api']:
281 res.status = b'200 OK'
282 res.headers[b'Content-Type'] = b'text/plain'
283 lines = [
284 _(
285 b'APIs can be accessed at /api/<name>, where <name> can be '
286 b'one of the following:\n'
287 )
288 ]
289 if availableapis:
290 lines.extend(sorted(availableapis))
291 else:
292 lines.append(_(b'(no available APIs)\n'))
293 res.setbodybytes(b'\n'.join(lines))
294 return
295
296 proto = req.dispatchparts[1]
297
298 if proto not in API_HANDLERS:
299 res.status = b'404 Not Found'
300 res.headers[b'Content-Type'] = b'text/plain'
301 res.setbodybytes(
302 _(b'Unknown API: %s\nKnown APIs: %s')
303 % (proto, b', '.join(sorted(availableapis)))
304 )
305 return
306
307 if proto not in availableapis:
308 res.status = b'404 Not Found'
309 res.headers[b'Content-Type'] = b'text/plain'
310 res.setbodybytes(_(b'API %s not enabled\n') % proto)
311 return
312
313 API_HANDLERS[proto][b'handler'](
314 rctx, req, res, checkperm, req.dispatchparts[2:]
315 )
316
317
318 # Maps API name to metadata so custom API can be registered.
319 # Keys are:
320 #
321 # config
322 # Config option that controls whether service is enabled.
323 # handler
324 # Callable receiving (rctx, req, res, checkperm, urlparts) that is called
325 # when a request to this API is received.
326 # apidescriptor
327 # Callable receiving (req, repo) that is called to obtain an API
328 # descriptor for this service. The response must be serializable to CBOR.
329 API_HANDLERS = {
330 wireprotov2server.HTTP_WIREPROTO_V2: {
331 b'config': (b'experimental', b'web.api.http-v2'),
332 b'handler': wireprotov2server.handlehttpv2request,
333 b'apidescriptor': wireprotov2server.httpv2apidescriptor,
334 },
335 }
336
337
338 def _httpresponsetype(ui, proto, prefer_uncompressed): 244 def _httpresponsetype(ui, proto, prefer_uncompressed):
339 """Determine the appropriate response type and compression settings. 245 """Determine the appropriate response type and compression settings.
340 246
341 Returns a tuple of (mediatype, compengine, engineopts). 247 Returns a tuple of (mediatype, compengine, engineopts).
342 """ 248 """
369 # the server's network or CPU. 275 # the server's network or CPU.
370 opts = {b'level': ui.configint(b'server', b'zliblevel')} 276 opts = {b'level': ui.configint(b'server', b'zliblevel')}
371 return HGTYPE, util.compengines[b'zlib'], opts 277 return HGTYPE, util.compengines[b'zlib'], opts
372 278
373 279
374 def processcapabilitieshandshake(repo, req, res, proto):
375 """Called during a ?cmd=capabilities request.
376
377 If the client is advertising support for a newer protocol, we send
378 a CBOR response with information about available services. If no
379 advertised services are available, we don't handle the request.
380 """
381 # Fall back to old behavior unless the API server is enabled.
382 if not repo.ui.configbool(b'experimental', b'web.apiserver'):
383 return False
384
385 clientapis = decodevaluefromheaders(req, b'X-HgUpgrade')
386 protocaps = decodevaluefromheaders(req, b'X-HgProto')
387 if not clientapis or not protocaps:
388 return False
389
390 # We currently only support CBOR responses.
391 protocaps = set(protocaps.split(b' '))
392 if b'cbor' not in protocaps:
393 return False
394
395 descriptors = {}
396
397 for api in sorted(set(clientapis.split()) & _availableapis(repo)):
398 handler = API_HANDLERS[api]
399
400 descriptorfn = handler.get(b'apidescriptor')
401 if not descriptorfn:
402 continue
403
404 descriptors[api] = descriptorfn(req, repo)
405
406 v1caps = wireprotov1server.dispatch(repo, proto, b'capabilities')
407 assert isinstance(v1caps, wireprototypes.bytesresponse)
408
409 m = {
410 # TODO allow this to be configurable.
411 b'apibase': b'api/',
412 b'apis': descriptors,
413 b'v1capabilities': v1caps.data,
414 }
415
416 res.status = b'200 OK'
417 res.headers[b'Content-Type'] = b'application/mercurial-cbor'
418 res.setbodybytes(b''.join(cborutil.streamencode(m)))
419
420 return True
421
422
423 def _callhttp(repo, req, res, proto, cmd): 280 def _callhttp(repo, req, res, proto, cmd):
424 # Avoid cycle involving hg module. 281 # Avoid cycle involving hg module.
425 from .hgweb import common as hgwebcommon 282 from .hgweb import common as hgwebcommon
426 283
427 def genversion2(gen, engine, engineopts): 284 def genversion2(gen, engine, engineopts):
458 ), 315 ),
459 ) 316 )
460 return 317 return
461 318
462 proto.checkperm(wireprotov1server.commands[cmd].permission) 319 proto.checkperm(wireprotov1server.commands[cmd].permission)
463
464 # Possibly handle a modern client wanting to switch protocols.
465 if cmd == b'capabilities' and processcapabilitieshandshake(
466 repo, req, res, proto
467 ):
468
469 return
470 320
471 rsp = wireprotov1server.dispatch(repo, proto, cmd) 321 rsp = wireprotov1server.dispatch(repo, proto, cmd)
472 322
473 if isinstance(rsp, bytes): 323 if isinstance(rsp, bytes):
474 setresponse(HTTP_OK, HGTYPE, bodybytes=rsp) 324 setresponse(HTTP_OK, HGTYPE, bodybytes=rsp)
594 444
595 def checkperm(self, perm): 445 def checkperm(self, perm):
596 pass 446 pass
597 447
598 448
599 class sshv2protocolhandler(sshv1protocolhandler):
600 """Protocol handler for version 2 of the SSH protocol."""
601
602 @property
603 def name(self):
604 return wireprototypes.SSHV2
605
606 def addcapabilities(self, repo, caps):
607 return caps
608
609
610 def _runsshserver(ui, repo, fin, fout, ev): 449 def _runsshserver(ui, repo, fin, fout, ev):
611 # This function operates like a state machine of sorts. The following 450 # This function operates like a state machine of sorts. The following
612 # states are defined: 451 # states are defined:
613 # 452 #
614 # protov1-serving 453 # protov1-serving
615 # Server is in protocol version 1 serving mode. Commands arrive on 454 # Server is in protocol version 1 serving mode. Commands arrive on
616 # new lines. These commands are processed in this state, one command 455 # new lines. These commands are processed in this state, one command
617 # after the other. 456 # after the other.
618 # 457 #
619 # protov2-serving
620 # Server is in protocol version 2 serving mode.
621 #
622 # upgrade-initial
623 # The server is going to process an upgrade request.
624 #
625 # upgrade-v2-filter-legacy-handshake
626 # The protocol is being upgraded to version 2. The server is expecting
627 # the legacy handshake from version 1.
628 #
629 # upgrade-v2-finish
630 # The upgrade to version 2 of the protocol is imminent.
631 #
632 # shutdown 458 # shutdown
633 # The server is shutting down, possibly in reaction to a client event. 459 # The server is shutting down, possibly in reaction to a client event.
634 # 460 #
635 # And here are their transitions: 461 # And here are their transitions:
636 # 462 #
637 # protov1-serving -> shutdown 463 # protov1-serving -> shutdown
638 # When server receives an empty request or encounters another 464 # When server receives an empty request or encounters another
639 # error. 465 # error.
640 #
641 # protov1-serving -> upgrade-initial
642 # An upgrade request line was seen.
643 #
644 # upgrade-initial -> upgrade-v2-filter-legacy-handshake
645 # Upgrade to version 2 in progress. Server is expecting to
646 # process a legacy handshake.
647 #
648 # upgrade-v2-filter-legacy-handshake -> shutdown
649 # Client did not fulfill upgrade handshake requirements.
650 #
651 # upgrade-v2-filter-legacy-handshake -> upgrade-v2-finish
652 # Client fulfilled version 2 upgrade requirements. Finishing that
653 # upgrade.
654 #
655 # upgrade-v2-finish -> protov2-serving
656 # Protocol upgrade to version 2 complete. Server can now speak protocol
657 # version 2.
658 #
659 # protov2-serving -> protov1-serving
660 # Ths happens by default since protocol version 2 is the same as
661 # version 1 except for the handshake.
662 466
663 state = b'protov1-serving' 467 state = b'protov1-serving'
664 proto = sshv1protocolhandler(ui, fin, fout) 468 proto = sshv1protocolhandler(ui, fin, fout)
665 protoswitched = False
666 469
667 while not ev.is_set(): 470 while not ev.is_set():
668 if state == b'protov1-serving': 471 if state == b'protov1-serving':
669 # Commands are issued on new lines. 472 # Commands are issued on new lines.
670 request = fin.readline()[:-1] 473 request = fin.readline()[:-1]
671 474
672 # Empty lines signal to terminate the connection. 475 # Empty lines signal to terminate the connection.
673 if not request: 476 if not request:
674 state = b'shutdown' 477 state = b'shutdown'
675 continue
676
677 # It looks like a protocol upgrade request. Transition state to
678 # handle it.
679 if request.startswith(b'upgrade '):
680 if protoswitched:
681 _sshv1respondooberror(
682 fout,
683 ui.ferr,
684 b'cannot upgrade protocols multiple times',
685 )
686 state = b'shutdown'
687 continue
688
689 state = b'upgrade-initial'
690 continue 478 continue
691 479
692 available = wireprotov1server.commands.commandavailable( 480 available = wireprotov1server.commands.commandavailable(
693 request, proto 481 request, proto
694 ) 482 )
722 raise error.ProgrammingError( 510 raise error.ProgrammingError(
723 b'unhandled response type from ' 511 b'unhandled response type from '
724 b'wire protocol command: %s' % rsp 512 b'wire protocol command: %s' % rsp
725 ) 513 )
726 514
727 # For now, protocol version 2 serving just goes back to version 1.
728 elif state == b'protov2-serving':
729 state = b'protov1-serving'
730 continue
731
732 elif state == b'upgrade-initial':
733 # We should never transition into this state if we've switched
734 # protocols.
735 assert not protoswitched
736 assert proto.name == wireprototypes.SSHV1
737
738 # Expected: upgrade <token> <capabilities>
739 # If we get something else, the request is malformed. It could be
740 # from a future client that has altered the upgrade line content.
741 # We treat this as an unknown command.
742 try:
743 token, caps = request.split(b' ')[1:]
744 except ValueError:
745 _sshv1respondbytes(fout, b'')
746 state = b'protov1-serving'
747 continue
748
749 # Send empty response if we don't support upgrading protocols.
750 if not ui.configbool(b'experimental', b'sshserver.support-v2'):
751 _sshv1respondbytes(fout, b'')
752 state = b'protov1-serving'
753 continue
754
755 try:
756 caps = urlreq.parseqs(caps)
757 except ValueError:
758 _sshv1respondbytes(fout, b'')
759 state = b'protov1-serving'
760 continue
761
762 # We don't see an upgrade request to protocol version 2. Ignore
763 # the upgrade request.
764 wantedprotos = caps.get(b'proto', [b''])[0]
765 if SSHV2 not in wantedprotos:
766 _sshv1respondbytes(fout, b'')
767 state = b'protov1-serving'
768 continue
769
770 # It looks like we can honor this upgrade request to protocol 2.
771 # Filter the rest of the handshake protocol request lines.
772 state = b'upgrade-v2-filter-legacy-handshake'
773 continue
774
775 elif state == b'upgrade-v2-filter-legacy-handshake':
776 # Client should have sent legacy handshake after an ``upgrade``
777 # request. Expected lines:
778 #
779 # hello
780 # between
781 # pairs 81
782 # 0000...-0000...
783
784 ok = True
785 for line in (b'hello', b'between', b'pairs 81'):
786 request = fin.readline()[:-1]
787
788 if request != line:
789 _sshv1respondooberror(
790 fout,
791 ui.ferr,
792 b'malformed handshake protocol: missing %s' % line,
793 )
794 ok = False
795 state = b'shutdown'
796 break
797
798 if not ok:
799 continue
800
801 request = fin.read(81)
802 if request != b'%s-%s' % (b'0' * 40, b'0' * 40):
803 _sshv1respondooberror(
804 fout,
805 ui.ferr,
806 b'malformed handshake protocol: '
807 b'missing between argument value',
808 )
809 state = b'shutdown'
810 continue
811
812 state = b'upgrade-v2-finish'
813 continue
814
815 elif state == b'upgrade-v2-finish':
816 # Send the upgrade response.
817 fout.write(b'upgraded %s %s\n' % (token, SSHV2))
818 servercaps = wireprotov1server.capabilities(repo, proto)
819 rsp = b'capabilities: %s' % servercaps.data
820 fout.write(b'%d\n%s\n' % (len(rsp), rsp))
821 fout.flush()
822
823 proto = sshv2protocolhandler(ui, fin, fout)
824 protoswitched = True
825
826 state = b'protov2-serving'
827 continue
828
829 elif state == b'shutdown': 515 elif state == b'shutdown':
830 break 516 break
831 517
832 else: 518 else:
833 raise error.ProgrammingError( 519 raise error.ProgrammingError(