40 HGTYPE2 = 'application/mercurial-0.2' |
38 HGTYPE2 = 'application/mercurial-0.2' |
41 HGERRTYPE = 'application/hg-error' |
39 HGERRTYPE = 'application/hg-error' |
42 |
40 |
43 SSHV1 = wireprototypes.SSHV1 |
41 SSHV1 = wireprototypes.SSHV1 |
44 SSHV2 = wireprototypes.SSHV2 |
42 SSHV2 = wireprototypes.SSHV2 |
|
43 |
45 |
44 |
46 def decodevaluefromheaders(req, headerprefix): |
45 def decodevaluefromheaders(req, headerprefix): |
47 """Decode a long value from multiple HTTP request headers. |
46 """Decode a long value from multiple HTTP request headers. |
48 |
47 |
49 Returns the value as a bytes, not a str. |
48 Returns the value as a bytes, not a str. |
88 |
88 |
89 def _args(self): |
89 def _args(self): |
90 args = self._req.qsparams.asdictoflists() |
90 args = self._req.qsparams.asdictoflists() |
91 postlen = int(self._req.headers.get(b'X-HgArgs-Post', 0)) |
91 postlen = int(self._req.headers.get(b'X-HgArgs-Post', 0)) |
92 if postlen: |
92 if postlen: |
93 args.update(urlreq.parseqs( |
93 args.update( |
94 self._req.bodyfh.read(postlen), keep_blank_values=True)) |
94 urlreq.parseqs( |
|
95 self._req.bodyfh.read(postlen), keep_blank_values=True |
|
96 ) |
|
97 ) |
95 return args |
98 return args |
96 |
99 |
97 argvalue = decodevaluefromheaders(self._req, b'X-HgArg') |
100 argvalue = decodevaluefromheaders(self._req, b'X-HgArg') |
98 args.update(urlreq.parseqs(argvalue, keep_blank_values=True)) |
101 args.update(urlreq.parseqs(argvalue, keep_blank_values=True)) |
99 return args |
102 return args |
130 |
133 |
131 def client(self): |
134 def client(self): |
132 return 'remote:%s:%s:%s' % ( |
135 return 'remote:%s:%s:%s' % ( |
133 self._req.urlscheme, |
136 self._req.urlscheme, |
134 urlreq.quote(self._req.remotehost or ''), |
137 urlreq.quote(self._req.remotehost or ''), |
135 urlreq.quote(self._req.remoteuser or '')) |
138 urlreq.quote(self._req.remoteuser or ''), |
|
139 ) |
136 |
140 |
137 def addcapabilities(self, repo, caps): |
141 def addcapabilities(self, repo, caps): |
138 caps.append(b'batch') |
142 caps.append(b'batch') |
139 |
143 |
140 caps.append('httpheader=%d' % |
144 caps.append( |
141 repo.ui.configint('server', 'maxhttpheaderlen')) |
145 'httpheader=%d' % repo.ui.configint('server', 'maxhttpheaderlen') |
|
146 ) |
142 if repo.ui.configbool('experimental', 'httppostargs'): |
147 if repo.ui.configbool('experimental', 'httppostargs'): |
143 caps.append('httppostargs') |
148 caps.append('httppostargs') |
144 |
149 |
145 # FUTURE advertise 0.2rx once support is implemented |
150 # FUTURE advertise 0.2rx once support is implemented |
146 # FUTURE advertise minrx and mintx after consulting config option |
151 # FUTURE advertise minrx and mintx after consulting config option |
147 caps.append('httpmediatype=0.1rx,0.1tx,0.2tx') |
152 caps.append('httpmediatype=0.1rx,0.1tx,0.2tx') |
148 |
153 |
149 compengines = wireprototypes.supportedcompengines(repo.ui, |
154 compengines = wireprototypes.supportedcompengines( |
150 compression.SERVERROLE) |
155 repo.ui, compression.SERVERROLE |
|
156 ) |
151 if compengines: |
157 if compengines: |
152 comptypes = ','.join(urlreq.quote(e.wireprotosupport().name) |
158 comptypes = ','.join( |
153 for e in compengines) |
159 urlreq.quote(e.wireprotosupport().name) for e in compengines |
|
160 ) |
154 caps.append('compression=%s' % comptypes) |
161 caps.append('compression=%s' % comptypes) |
155 |
162 |
156 return caps |
163 return caps |
157 |
164 |
158 def checkperm(self, perm): |
165 def checkperm(self, perm): |
159 return self._checkperm(perm) |
166 return self._checkperm(perm) |
|
167 |
160 |
168 |
161 # This method exists mostly so that extensions like remotefilelog can |
169 # This method exists mostly so that extensions like remotefilelog can |
162 # disable a kludgey legacy method only over http. As of early 2018, |
170 # disable a kludgey legacy method only over http. As of early 2018, |
163 # there are no other known users, so with any luck we can discard this |
171 # there are no other known users, so with any luck we can discard this |
164 # hook if remotefilelog becomes a first-party extension. |
172 # hook if remotefilelog becomes a first-party extension. |
165 def iscmd(cmd): |
173 def iscmd(cmd): |
166 return cmd in wireprotov1server.commands |
174 return cmd in wireprotov1server.commands |
|
175 |
167 |
176 |
168 def handlewsgirequest(rctx, req, res, checkperm): |
177 def handlewsgirequest(rctx, req, res, checkperm): |
169 """Possibly process a wire protocol request. |
178 """Possibly process a wire protocol request. |
170 |
179 |
171 If the current request is a wire protocol request, the request is |
180 If the current request is a wire protocol request, the request is |
210 # TODO This is not a good response to issue for this request. This |
219 # TODO This is not a good response to issue for this request. This |
211 # is mostly for BC for now. |
220 # is mostly for BC for now. |
212 res.setbodybytes('0\n%s\n' % b'Not Found') |
221 res.setbodybytes('0\n%s\n' % b'Not Found') |
213 return True |
222 return True |
214 |
223 |
215 proto = httpv1protocolhandler(req, repo.ui, |
224 proto = httpv1protocolhandler( |
216 lambda perm: checkperm(rctx, req, perm)) |
225 req, repo.ui, lambda perm: checkperm(rctx, req, perm) |
|
226 ) |
217 |
227 |
218 # The permissions checker should be the only thing that can raise an |
228 # The permissions checker should be the only thing that can raise an |
219 # ErrorResponse. It is kind of a layer violation to catch an hgweb |
229 # ErrorResponse. It is kind of a layer violation to catch an hgweb |
220 # exception here. So consider refactoring into a exception type that |
230 # exception here. So consider refactoring into a exception type that |
221 # is associated with the wire protocol. |
231 # is associated with the wire protocol. |
264 |
276 |
265 # Requests to /api/ list available APIs. |
277 # Requests to /api/ list available APIs. |
266 if req.dispatchparts == [b'api']: |
278 if req.dispatchparts == [b'api']: |
267 res.status = b'200 OK' |
279 res.status = b'200 OK' |
268 res.headers[b'Content-Type'] = b'text/plain' |
280 res.headers[b'Content-Type'] = b'text/plain' |
269 lines = [_('APIs can be accessed at /api/<name>, where <name> can be ' |
281 lines = [ |
270 'one of the following:\n')] |
282 _( |
|
283 'APIs can be accessed at /api/<name>, where <name> can be ' |
|
284 'one of the following:\n' |
|
285 ) |
|
286 ] |
271 if availableapis: |
287 if availableapis: |
272 lines.extend(sorted(availableapis)) |
288 lines.extend(sorted(availableapis)) |
273 else: |
289 else: |
274 lines.append(_('(no available APIs)\n')) |
290 lines.append(_('(no available APIs)\n')) |
275 res.setbodybytes(b'\n'.join(lines)) |
291 res.setbodybytes(b'\n'.join(lines)) |
278 proto = req.dispatchparts[1] |
294 proto = req.dispatchparts[1] |
279 |
295 |
280 if proto not in API_HANDLERS: |
296 if proto not in API_HANDLERS: |
281 res.status = b'404 Not Found' |
297 res.status = b'404 Not Found' |
282 res.headers[b'Content-Type'] = b'text/plain' |
298 res.headers[b'Content-Type'] = b'text/plain' |
283 res.setbodybytes(_('Unknown API: %s\nKnown APIs: %s') % ( |
299 res.setbodybytes( |
284 proto, b', '.join(sorted(availableapis)))) |
300 _('Unknown API: %s\nKnown APIs: %s') |
|
301 % (proto, b', '.join(sorted(availableapis))) |
|
302 ) |
285 return |
303 return |
286 |
304 |
287 if proto not in availableapis: |
305 if proto not in availableapis: |
288 res.status = b'404 Not Found' |
306 res.status = b'404 Not Found' |
289 res.headers[b'Content-Type'] = b'text/plain' |
307 res.headers[b'Content-Type'] = b'text/plain' |
290 res.setbodybytes(_('API %s not enabled\n') % proto) |
308 res.setbodybytes(_('API %s not enabled\n') % proto) |
291 return |
309 return |
292 |
310 |
293 API_HANDLERS[proto]['handler'](rctx, req, res, checkperm, |
311 API_HANDLERS[proto]['handler']( |
294 req.dispatchparts[2:]) |
312 rctx, req, res, checkperm, req.dispatchparts[2:] |
|
313 ) |
|
314 |
295 |
315 |
296 # Maps API name to metadata so custom API can be registered. |
316 # Maps API name to metadata so custom API can be registered. |
297 # Keys are: |
317 # Keys are: |
298 # |
318 # |
299 # config |
319 # config |
310 'handler': wireprotov2server.handlehttpv2request, |
330 'handler': wireprotov2server.handlehttpv2request, |
311 'apidescriptor': wireprotov2server.httpv2apidescriptor, |
331 'apidescriptor': wireprotov2server.httpv2apidescriptor, |
312 }, |
332 }, |
313 } |
333 } |
314 |
334 |
|
335 |
315 def _httpresponsetype(ui, proto, prefer_uncompressed): |
336 def _httpresponsetype(ui, proto, prefer_uncompressed): |
316 """Determine the appropriate response type and compression settings. |
337 """Determine the appropriate response type and compression settings. |
317 |
338 |
318 Returns a tuple of (mediatype, compengine, engineopts). |
339 Returns a tuple of (mediatype, compengine, engineopts). |
319 """ |
340 """ |
325 if prefer_uncompressed: |
346 if prefer_uncompressed: |
326 return HGTYPE2, compression._noopengine(), {} |
347 return HGTYPE2, compression._noopengine(), {} |
327 |
348 |
328 # Now find an agreed upon compression format. |
349 # Now find an agreed upon compression format. |
329 compformats = wireprotov1server.clientcompressionsupport(proto) |
350 compformats = wireprotov1server.clientcompressionsupport(proto) |
330 for engine in wireprototypes.supportedcompengines(ui, |
351 for engine in wireprototypes.supportedcompengines( |
331 compression.SERVERROLE): |
352 ui, compression.SERVERROLE |
|
353 ): |
332 if engine.wireprotosupport().name in compformats: |
354 if engine.wireprotosupport().name in compformats: |
333 opts = {} |
355 opts = {} |
334 level = ui.configint('server', '%slevel' % engine.name()) |
356 level = ui.configint('server', '%slevel' % engine.name()) |
335 if level is not None: |
357 if level is not None: |
336 opts['level'] = level |
358 opts['level'] = level |
343 # Don't allow untrusted settings because disabling compression or |
365 # Don't allow untrusted settings because disabling compression or |
344 # setting a very high compression level could lead to flooding |
366 # setting a very high compression level could lead to flooding |
345 # the server's network or CPU. |
367 # the server's network or CPU. |
346 opts = {'level': ui.configint('server', 'zliblevel')} |
368 opts = {'level': ui.configint('server', 'zliblevel')} |
347 return HGTYPE, util.compengines['zlib'], opts |
369 return HGTYPE, util.compengines['zlib'], opts |
|
370 |
348 |
371 |
349 def processcapabilitieshandshake(repo, req, res, proto): |
372 def processcapabilitieshandshake(repo, req, res, proto): |
350 """Called during a ?cmd=capabilities request. |
373 """Called during a ?cmd=capabilities request. |
351 |
374 |
352 If the client is advertising support for a newer protocol, we send |
375 If the client is advertising support for a newer protocol, we send |
392 res.headers[b'Content-Type'] = b'application/mercurial-cbor' |
415 res.headers[b'Content-Type'] = b'application/mercurial-cbor' |
393 res.setbodybytes(b''.join(cborutil.streamencode(m))) |
416 res.setbodybytes(b''.join(cborutil.streamencode(m))) |
394 |
417 |
395 return True |
418 return True |
396 |
419 |
|
420 |
397 def _callhttp(repo, req, res, proto, cmd): |
421 def _callhttp(repo, req, res, proto, cmd): |
398 # Avoid cycle involving hg module. |
422 # Avoid cycle involving hg module. |
399 from .hgweb import common as hgwebcommon |
423 from .hgweb import common as hgwebcommon |
400 |
424 |
401 def genversion2(gen, engine, engineopts): |
425 def genversion2(gen, engine, engineopts): |
421 res.setbodybytes(bodybytes) |
445 res.setbodybytes(bodybytes) |
422 if bodygen is not None: |
446 if bodygen is not None: |
423 res.setbodygen(bodygen) |
447 res.setbodygen(bodygen) |
424 |
448 |
425 if not wireprotov1server.commands.commandavailable(cmd, proto): |
449 if not wireprotov1server.commands.commandavailable(cmd, proto): |
426 setresponse(HTTP_OK, HGERRTYPE, |
450 setresponse( |
427 _('requested wire protocol command is not available over ' |
451 HTTP_OK, |
428 'HTTP')) |
452 HGERRTYPE, |
|
453 _('requested wire protocol command is not available over ' 'HTTP'), |
|
454 ) |
429 return |
455 return |
430 |
456 |
431 proto.checkperm(wireprotov1server.commands[cmd].permission) |
457 proto.checkperm(wireprotov1server.commands[cmd].permission) |
432 |
458 |
433 # Possibly handle a modern client wanting to switch protocols. |
459 # Possibly handle a modern client wanting to switch protocols. |
434 if (cmd == 'capabilities' and |
460 if cmd == 'capabilities' and processcapabilitieshandshake( |
435 processcapabilitieshandshake(repo, req, res, proto)): |
461 repo, req, res, proto |
|
462 ): |
436 |
463 |
437 return |
464 return |
438 |
465 |
439 rsp = wireprotov1server.dispatch(repo, proto, cmd) |
466 rsp = wireprotov1server.dispatch(repo, proto, cmd) |
440 |
467 |
448 gen = rsp.gen |
475 gen = rsp.gen |
449 |
476 |
450 # This code for compression should not be streamres specific. It |
477 # This code for compression should not be streamres specific. It |
451 # is here because we only compress streamres at the moment. |
478 # is here because we only compress streamres at the moment. |
452 mediatype, engine, engineopts = _httpresponsetype( |
479 mediatype, engine, engineopts = _httpresponsetype( |
453 repo.ui, proto, rsp.prefer_uncompressed) |
480 repo.ui, proto, rsp.prefer_uncompressed |
|
481 ) |
454 gen = engine.compressstream(gen, engineopts) |
482 gen = engine.compressstream(gen, engineopts) |
455 |
483 |
456 if mediatype == HGTYPE2: |
484 if mediatype == HGTYPE2: |
457 gen = genversion2(gen, engine, engineopts) |
485 gen = genversion2(gen, engine, engineopts) |
458 |
486 |
467 elif isinstance(rsp, wireprototypes.ooberror): |
495 elif isinstance(rsp, wireprototypes.ooberror): |
468 setresponse(HTTP_OK, HGERRTYPE, bodybytes=rsp.message) |
496 setresponse(HTTP_OK, HGERRTYPE, bodybytes=rsp.message) |
469 else: |
497 else: |
470 raise error.ProgrammingError('hgweb.protocol internal failure', rsp) |
498 raise error.ProgrammingError('hgweb.protocol internal failure', rsp) |
471 |
499 |
|
500 |
472 def _sshv1respondbytes(fout, value): |
501 def _sshv1respondbytes(fout, value): |
473 """Send a bytes response for protocol version 1.""" |
502 """Send a bytes response for protocol version 1.""" |
474 fout.write('%d\n' % len(value)) |
503 fout.write('%d\n' % len(value)) |
475 fout.write(value) |
504 fout.write(value) |
476 fout.flush() |
505 fout.flush() |
477 |
506 |
|
507 |
478 def _sshv1respondstream(fout, source): |
508 def _sshv1respondstream(fout, source): |
479 write = fout.write |
509 write = fout.write |
480 for chunk in source.gen: |
510 for chunk in source.gen: |
481 write(chunk) |
511 write(chunk) |
482 fout.flush() |
512 fout.flush() |
483 |
513 |
|
514 |
484 def _sshv1respondooberror(fout, ferr, rsp): |
515 def _sshv1respondooberror(fout, ferr, rsp): |
485 ferr.write(b'%s\n-\n' % rsp) |
516 ferr.write(b'%s\n-\n' % rsp) |
486 ferr.flush() |
517 ferr.flush() |
487 fout.write(b'\n') |
518 fout.write(b'\n') |
488 fout.flush() |
519 fout.flush() |
489 |
520 |
|
521 |
490 @interfaceutil.implementer(wireprototypes.baseprotocolhandler) |
522 @interfaceutil.implementer(wireprototypes.baseprotocolhandler) |
491 class sshv1protocolhandler(object): |
523 class sshv1protocolhandler(object): |
492 """Handler for requests services via version 1 of SSH protocol.""" |
524 """Handler for requests services via version 1 of SSH protocol.""" |
|
525 |
493 def __init__(self, ui, fin, fout): |
526 def __init__(self, ui, fin, fout): |
494 self._ui = ui |
527 self._ui = ui |
495 self._fin = fin |
528 self._fin = fin |
496 self._fout = fout |
529 self._fout = fout |
497 self._protocaps = set() |
530 self._protocaps = set() |
555 return caps |
588 return caps |
556 |
589 |
557 def checkperm(self, perm): |
590 def checkperm(self, perm): |
558 pass |
591 pass |
559 |
592 |
|
593 |
560 class sshv2protocolhandler(sshv1protocolhandler): |
594 class sshv2protocolhandler(sshv1protocolhandler): |
561 """Protocol handler for version 2 of the SSH protocol.""" |
595 """Protocol handler for version 2 of the SSH protocol.""" |
562 |
596 |
563 @property |
597 @property |
564 def name(self): |
598 def name(self): |
565 return wireprototypes.SSHV2 |
599 return wireprototypes.SSHV2 |
566 |
600 |
567 def addcapabilities(self, repo, caps): |
601 def addcapabilities(self, repo, caps): |
568 return caps |
602 return caps |
|
603 |
569 |
604 |
570 def _runsshserver(ui, repo, fin, fout, ev): |
605 def _runsshserver(ui, repo, fin, fout, ev): |
571 # This function operates like a state machine of sorts. The following |
606 # This function operates like a state machine of sorts. The following |
572 # states are defined: |
607 # states are defined: |
573 # |
608 # |
636 |
671 |
637 # It looks like a protocol upgrade request. Transition state to |
672 # It looks like a protocol upgrade request. Transition state to |
638 # handle it. |
673 # handle it. |
639 if request.startswith(b'upgrade '): |
674 if request.startswith(b'upgrade '): |
640 if protoswitched: |
675 if protoswitched: |
641 _sshv1respondooberror(fout, ui.ferr, |
676 _sshv1respondooberror( |
642 b'cannot upgrade protocols multiple ' |
677 fout, |
643 b'times') |
678 ui.ferr, |
|
679 b'cannot upgrade protocols multiple ' b'times', |
|
680 ) |
644 state = 'shutdown' |
681 state = 'shutdown' |
645 continue |
682 continue |
646 |
683 |
647 state = 'upgrade-initial' |
684 state = 'upgrade-initial' |
648 continue |
685 continue |
649 |
686 |
650 available = wireprotov1server.commands.commandavailable( |
687 available = wireprotov1server.commands.commandavailable( |
651 request, proto) |
688 request, proto |
|
689 ) |
652 |
690 |
653 # This command isn't available. Send an empty response and go |
691 # This command isn't available. Send an empty response and go |
654 # back to waiting for a new command. |
692 # back to waiting for a new command. |
655 if not available: |
693 if not available: |
656 _sshv1respondbytes(fout, b'') |
694 _sshv1respondbytes(fout, b'') |
674 elif isinstance(rsp, wireprototypes.pusherr): |
712 elif isinstance(rsp, wireprototypes.pusherr): |
675 _sshv1respondbytes(fout, rsp.res) |
713 _sshv1respondbytes(fout, rsp.res) |
676 elif isinstance(rsp, wireprototypes.ooberror): |
714 elif isinstance(rsp, wireprototypes.ooberror): |
677 _sshv1respondooberror(fout, ui.ferr, rsp.message) |
715 _sshv1respondooberror(fout, ui.ferr, rsp.message) |
678 else: |
716 else: |
679 raise error.ProgrammingError('unhandled response type from ' |
717 raise error.ProgrammingError( |
680 'wire protocol command: %s' % rsp) |
718 'unhandled response type from ' |
|
719 'wire protocol command: %s' % rsp |
|
720 ) |
681 |
721 |
682 # For now, protocol version 2 serving just goes back to version 1. |
722 # For now, protocol version 2 serving just goes back to version 1. |
683 elif state == 'protov2-serving': |
723 elif state == 'protov2-serving': |
684 state = 'protov1-serving' |
724 state = 'protov1-serving' |
685 continue |
725 continue |
739 ok = True |
779 ok = True |
740 for line in (b'hello', b'between', b'pairs 81'): |
780 for line in (b'hello', b'between', b'pairs 81'): |
741 request = fin.readline()[:-1] |
781 request = fin.readline()[:-1] |
742 |
782 |
743 if request != line: |
783 if request != line: |
744 _sshv1respondooberror(fout, ui.ferr, |
784 _sshv1respondooberror( |
745 b'malformed handshake protocol: ' |
785 fout, |
746 b'missing %s' % line) |
786 ui.ferr, |
|
787 b'malformed handshake protocol: ' b'missing %s' % line, |
|
788 ) |
747 ok = False |
789 ok = False |
748 state = 'shutdown' |
790 state = 'shutdown' |
749 break |
791 break |
750 |
792 |
751 if not ok: |
793 if not ok: |
752 continue |
794 continue |
753 |
795 |
754 request = fin.read(81) |
796 request = fin.read(81) |
755 if request != b'%s-%s' % (b'0' * 40, b'0' * 40): |
797 if request != b'%s-%s' % (b'0' * 40, b'0' * 40): |
756 _sshv1respondooberror(fout, ui.ferr, |
798 _sshv1respondooberror( |
757 b'malformed handshake protocol: ' |
799 fout, |
758 b'missing between argument value') |
800 ui.ferr, |
|
801 b'malformed handshake protocol: ' |
|
802 b'missing between argument value', |
|
803 ) |
759 state = 'shutdown' |
804 state = 'shutdown' |
760 continue |
805 continue |
761 |
806 |
762 state = 'upgrade-v2-finish' |
807 state = 'upgrade-v2-finish' |
763 continue |
808 continue |
778 |
823 |
779 elif state == 'shutdown': |
824 elif state == 'shutdown': |
780 break |
825 break |
781 |
826 |
782 else: |
827 else: |
783 raise error.ProgrammingError('unhandled ssh server state: %s' % |
828 raise error.ProgrammingError( |
784 state) |
829 'unhandled ssh server state: %s' % state |
|
830 ) |
|
831 |
785 |
832 |
786 class sshserver(object): |
833 class sshserver(object): |
787 def __init__(self, ui, repo, logfh=None): |
834 def __init__(self, ui, repo, logfh=None): |
788 self._ui = ui |
835 self._ui = ui |
789 self._repo = repo |
836 self._repo = repo |
790 self._fin, self._fout = ui.protectfinout() |
837 self._fin, self._fout = ui.protectfinout() |
791 |
838 |
792 # Log write I/O to stdout and stderr if configured. |
839 # Log write I/O to stdout and stderr if configured. |
793 if logfh: |
840 if logfh: |
794 self._fout = util.makeloggingfileobject( |
841 self._fout = util.makeloggingfileobject( |
795 logfh, self._fout, 'o', logdata=True) |
842 logfh, self._fout, 'o', logdata=True |
|
843 ) |
796 ui.ferr = util.makeloggingfileobject( |
844 ui.ferr = util.makeloggingfileobject( |
797 logfh, ui.ferr, 'e', logdata=True) |
845 logfh, ui.ferr, 'e', logdata=True |
|
846 ) |
798 |
847 |
799 def serve_forever(self): |
848 def serve_forever(self): |
800 self.serveuntil(threading.Event()) |
849 self.serveuntil(threading.Event()) |
801 self._ui.restorefinout(self._fin, self._fout) |
850 self._ui.restorefinout(self._fin, self._fout) |
802 sys.exit(0) |
851 sys.exit(0) |