Mercurial > public > mercurial-scm > hg-stable
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( |