##// END OF EJS Templates
wireprototypes: move baseprotocolhandler from wireprotoserver...
Gregory Szorc -
r36389:0c231df1 default
parent child Browse files
Show More
@@ -1,651 +1,601 b''
1 1 # Copyright 21 May 2005 - (c) 2005 Jake Edge <jake@edge2.net>
2 2 # Copyright 2005-2007 Matt Mackall <mpm@selenic.com>
3 3 #
4 4 # This software may be used and distributed according to the terms of the
5 5 # GNU General Public License version 2 or any later version.
6 6
7 7 from __future__ import absolute_import
8 8
9 import abc
10 9 import contextlib
11 10 import struct
12 11 import sys
13 12
14 13 from .i18n import _
15 14 from . import (
16 15 encoding,
17 16 error,
18 17 hook,
19 18 pycompat,
20 19 util,
21 20 wireproto,
22 21 wireprototypes,
23 22 )
24 23
25 24 stringio = util.stringio
26 25
27 26 urlerr = util.urlerr
28 27 urlreq = util.urlreq
29 28
30 29 HTTP_OK = 200
31 30
32 31 HGTYPE = 'application/mercurial-0.1'
33 32 HGTYPE2 = 'application/mercurial-0.2'
34 33 HGERRTYPE = 'application/hg-error'
35 34
36 35 # Names of the SSH protocol implementations.
37 36 SSHV1 = 'ssh-v1'
38 37 # This is advertised over the wire. Incremental the counter at the end
39 38 # to reflect BC breakages.
40 39 SSHV2 = 'exp-ssh-v2-0001'
41 40
42 class baseprotocolhandler(object):
43 """Abstract base class for wire protocol handlers.
44
45 A wire protocol handler serves as an interface between protocol command
46 handlers and the wire protocol transport layer. Protocol handlers provide
47 methods to read command arguments, redirect stdio for the duration of
48 the request, handle response types, etc.
49 """
50
51 __metaclass__ = abc.ABCMeta
52
53 @abc.abstractproperty
54 def name(self):
55 """The name of the protocol implementation.
56
57 Used for uniquely identifying the transport type.
58 """
59
60 @abc.abstractmethod
61 def getargs(self, args):
62 """return the value for arguments in <args>
63
64 returns a list of values (same order as <args>)"""
65
66 @abc.abstractmethod
67 def forwardpayload(self, fp):
68 """Read the raw payload and forward to a file.
69
70 The payload is read in full before the function returns.
71 """
72
73 @abc.abstractmethod
74 def mayberedirectstdio(self):
75 """Context manager to possibly redirect stdio.
76
77 The context manager yields a file-object like object that receives
78 stdout and stderr output when the context manager is active. Or it
79 yields ``None`` if no I/O redirection occurs.
80
81 The intent of this context manager is to capture stdio output
82 so it may be sent in the response. Some transports support streaming
83 stdio to the client in real time. For these transports, stdio output
84 won't be captured.
85 """
86
87 @abc.abstractmethod
88 def client(self):
89 """Returns a string representation of this client (as bytes)."""
90
91 41 def decodevaluefromheaders(req, headerprefix):
92 42 """Decode a long value from multiple HTTP request headers.
93 43
94 44 Returns the value as a bytes, not a str.
95 45 """
96 46 chunks = []
97 47 i = 1
98 48 prefix = headerprefix.upper().replace(r'-', r'_')
99 49 while True:
100 50 v = req.env.get(r'HTTP_%s_%d' % (prefix, i))
101 51 if v is None:
102 52 break
103 53 chunks.append(pycompat.bytesurl(v))
104 54 i += 1
105 55
106 56 return ''.join(chunks)
107 57
108 class httpv1protocolhandler(baseprotocolhandler):
58 class httpv1protocolhandler(wireprototypes.baseprotocolhandler):
109 59 def __init__(self, req, ui):
110 60 self._req = req
111 61 self._ui = ui
112 62
113 63 @property
114 64 def name(self):
115 65 return 'http-v1'
116 66
117 67 def getargs(self, args):
118 68 knownargs = self._args()
119 69 data = {}
120 70 keys = args.split()
121 71 for k in keys:
122 72 if k == '*':
123 73 star = {}
124 74 for key in knownargs.keys():
125 75 if key != 'cmd' and key not in keys:
126 76 star[key] = knownargs[key][0]
127 77 data['*'] = star
128 78 else:
129 79 data[k] = knownargs[k][0]
130 80 return [data[k] for k in keys]
131 81
132 82 def _args(self):
133 83 args = util.rapply(pycompat.bytesurl, self._req.form.copy())
134 84 postlen = int(self._req.env.get(r'HTTP_X_HGARGS_POST', 0))
135 85 if postlen:
136 86 args.update(urlreq.parseqs(
137 87 self._req.read(postlen), keep_blank_values=True))
138 88 return args
139 89
140 90 argvalue = decodevaluefromheaders(self._req, r'X-HgArg')
141 91 args.update(urlreq.parseqs(argvalue, keep_blank_values=True))
142 92 return args
143 93
144 94 def forwardpayload(self, fp):
145 95 if r'HTTP_CONTENT_LENGTH' in self._req.env:
146 96 length = int(self._req.env[r'HTTP_CONTENT_LENGTH'])
147 97 else:
148 98 length = int(self._req.env[r'CONTENT_LENGTH'])
149 99 # If httppostargs is used, we need to read Content-Length
150 100 # minus the amount that was consumed by args.
151 101 length -= int(self._req.env.get(r'HTTP_X_HGARGS_POST', 0))
152 102 for s in util.filechunkiter(self._req, limit=length):
153 103 fp.write(s)
154 104
155 105 @contextlib.contextmanager
156 106 def mayberedirectstdio(self):
157 107 oldout = self._ui.fout
158 108 olderr = self._ui.ferr
159 109
160 110 out = util.stringio()
161 111
162 112 try:
163 113 self._ui.fout = out
164 114 self._ui.ferr = out
165 115 yield out
166 116 finally:
167 117 self._ui.fout = oldout
168 118 self._ui.ferr = olderr
169 119
170 120 def client(self):
171 121 return 'remote:%s:%s:%s' % (
172 122 self._req.env.get('wsgi.url_scheme') or 'http',
173 123 urlreq.quote(self._req.env.get('REMOTE_HOST', '')),
174 124 urlreq.quote(self._req.env.get('REMOTE_USER', '')))
175 125
176 126 # This method exists mostly so that extensions like remotefilelog can
177 127 # disable a kludgey legacy method only over http. As of early 2018,
178 128 # there are no other known users, so with any luck we can discard this
179 129 # hook if remotefilelog becomes a first-party extension.
180 130 def iscmd(cmd):
181 131 return cmd in wireproto.commands
182 132
183 133 def parsehttprequest(repo, req, query):
184 134 """Parse the HTTP request for a wire protocol request.
185 135
186 136 If the current request appears to be a wire protocol request, this
187 137 function returns a dict with details about that request, including
188 138 an ``abstractprotocolserver`` instance suitable for handling the
189 139 request. Otherwise, ``None`` is returned.
190 140
191 141 ``req`` is a ``wsgirequest`` instance.
192 142 """
193 143 # HTTP version 1 wire protocol requests are denoted by a "cmd" query
194 144 # string parameter. If it isn't present, this isn't a wire protocol
195 145 # request.
196 146 if r'cmd' not in req.form:
197 147 return None
198 148
199 149 cmd = pycompat.sysbytes(req.form[r'cmd'][0])
200 150
201 151 # The "cmd" request parameter is used by both the wire protocol and hgweb.
202 152 # While not all wire protocol commands are available for all transports,
203 153 # if we see a "cmd" value that resembles a known wire protocol command, we
204 154 # route it to a protocol handler. This is better than routing possible
205 155 # wire protocol requests to hgweb because it prevents hgweb from using
206 156 # known wire protocol commands and it is less confusing for machine
207 157 # clients.
208 158 if not iscmd(cmd):
209 159 return None
210 160
211 161 proto = httpv1protocolhandler(req, repo.ui)
212 162
213 163 return {
214 164 'cmd': cmd,
215 165 'proto': proto,
216 166 'dispatch': lambda: _callhttp(repo, req, proto, cmd),
217 167 'handleerror': lambda ex: _handlehttperror(ex, req, cmd),
218 168 }
219 169
220 170 def _httpresponsetype(ui, req, prefer_uncompressed):
221 171 """Determine the appropriate response type and compression settings.
222 172
223 173 Returns a tuple of (mediatype, compengine, engineopts).
224 174 """
225 175 # Determine the response media type and compression engine based
226 176 # on the request parameters.
227 177 protocaps = decodevaluefromheaders(req, r'X-HgProto').split(' ')
228 178
229 179 if '0.2' in protocaps:
230 180 # All clients are expected to support uncompressed data.
231 181 if prefer_uncompressed:
232 182 return HGTYPE2, util._noopengine(), {}
233 183
234 184 # Default as defined by wire protocol spec.
235 185 compformats = ['zlib', 'none']
236 186 for cap in protocaps:
237 187 if cap.startswith('comp='):
238 188 compformats = cap[5:].split(',')
239 189 break
240 190
241 191 # Now find an agreed upon compression format.
242 192 for engine in wireproto.supportedcompengines(ui, util.SERVERROLE):
243 193 if engine.wireprotosupport().name in compformats:
244 194 opts = {}
245 195 level = ui.configint('server', '%slevel' % engine.name())
246 196 if level is not None:
247 197 opts['level'] = level
248 198
249 199 return HGTYPE2, engine, opts
250 200
251 201 # No mutually supported compression format. Fall back to the
252 202 # legacy protocol.
253 203
254 204 # Don't allow untrusted settings because disabling compression or
255 205 # setting a very high compression level could lead to flooding
256 206 # the server's network or CPU.
257 207 opts = {'level': ui.configint('server', 'zliblevel')}
258 208 return HGTYPE, util.compengines['zlib'], opts
259 209
260 210 def _callhttp(repo, req, proto, cmd):
261 211 def genversion2(gen, engine, engineopts):
262 212 # application/mercurial-0.2 always sends a payload header
263 213 # identifying the compression engine.
264 214 name = engine.wireprotosupport().name
265 215 assert 0 < len(name) < 256
266 216 yield struct.pack('B', len(name))
267 217 yield name
268 218
269 219 for chunk in gen:
270 220 yield chunk
271 221
272 222 rsp = wireproto.dispatch(repo, proto, cmd)
273 223
274 224 if not wireproto.commands.commandavailable(cmd, proto):
275 225 req.respond(HTTP_OK, HGERRTYPE,
276 226 body=_('requested wire protocol command is not available '
277 227 'over HTTP'))
278 228 return []
279 229
280 230 if isinstance(rsp, bytes):
281 231 req.respond(HTTP_OK, HGTYPE, body=rsp)
282 232 return []
283 233 elif isinstance(rsp, wireprototypes.bytesresponse):
284 234 req.respond(HTTP_OK, HGTYPE, body=rsp.data)
285 235 return []
286 236 elif isinstance(rsp, wireprototypes.streamreslegacy):
287 237 gen = rsp.gen
288 238 req.respond(HTTP_OK, HGTYPE)
289 239 return gen
290 240 elif isinstance(rsp, wireprototypes.streamres):
291 241 gen = rsp.gen
292 242
293 243 # This code for compression should not be streamres specific. It
294 244 # is here because we only compress streamres at the moment.
295 245 mediatype, engine, engineopts = _httpresponsetype(
296 246 repo.ui, req, rsp.prefer_uncompressed)
297 247 gen = engine.compressstream(gen, engineopts)
298 248
299 249 if mediatype == HGTYPE2:
300 250 gen = genversion2(gen, engine, engineopts)
301 251
302 252 req.respond(HTTP_OK, mediatype)
303 253 return gen
304 254 elif isinstance(rsp, wireprototypes.pushres):
305 255 rsp = '%d\n%s' % (rsp.res, rsp.output)
306 256 req.respond(HTTP_OK, HGTYPE, body=rsp)
307 257 return []
308 258 elif isinstance(rsp, wireprototypes.pusherr):
309 259 # This is the httplib workaround documented in _handlehttperror().
310 260 req.drain()
311 261
312 262 rsp = '0\n%s\n' % rsp.res
313 263 req.respond(HTTP_OK, HGTYPE, body=rsp)
314 264 return []
315 265 elif isinstance(rsp, wireprototypes.ooberror):
316 266 rsp = rsp.message
317 267 req.respond(HTTP_OK, HGERRTYPE, body=rsp)
318 268 return []
319 269 raise error.ProgrammingError('hgweb.protocol internal failure', rsp)
320 270
321 271 def _handlehttperror(e, req, cmd):
322 272 """Called when an ErrorResponse is raised during HTTP request processing."""
323 273
324 274 # Clients using Python's httplib are stateful: the HTTP client
325 275 # won't process an HTTP response until all request data is
326 276 # sent to the server. The intent of this code is to ensure
327 277 # we always read HTTP request data from the client, thus
328 278 # ensuring httplib transitions to a state that allows it to read
329 279 # the HTTP response. In other words, it helps prevent deadlocks
330 280 # on clients using httplib.
331 281
332 282 if (req.env[r'REQUEST_METHOD'] == r'POST' and
333 283 # But not if Expect: 100-continue is being used.
334 284 (req.env.get('HTTP_EXPECT',
335 285 '').lower() != '100-continue') or
336 286 # Or the non-httplib HTTP library is being advertised by
337 287 # the client.
338 288 req.env.get('X-HgHttp2', '')):
339 289 req.drain()
340 290 else:
341 291 req.headers.append((r'Connection', r'Close'))
342 292
343 293 # TODO This response body assumes the failed command was
344 294 # "unbundle." That assumption is not always valid.
345 295 req.respond(e, HGTYPE, body='0\n%s\n' % pycompat.bytestr(e))
346 296
347 297 return ''
348 298
349 299 def _sshv1respondbytes(fout, value):
350 300 """Send a bytes response for protocol version 1."""
351 301 fout.write('%d\n' % len(value))
352 302 fout.write(value)
353 303 fout.flush()
354 304
355 305 def _sshv1respondstream(fout, source):
356 306 write = fout.write
357 307 for chunk in source.gen:
358 308 write(chunk)
359 309 fout.flush()
360 310
361 311 def _sshv1respondooberror(fout, ferr, rsp):
362 312 ferr.write(b'%s\n-\n' % rsp)
363 313 ferr.flush()
364 314 fout.write(b'\n')
365 315 fout.flush()
366 316
367 class sshv1protocolhandler(baseprotocolhandler):
317 class sshv1protocolhandler(wireprototypes.baseprotocolhandler):
368 318 """Handler for requests services via version 1 of SSH protocol."""
369 319 def __init__(self, ui, fin, fout):
370 320 self._ui = ui
371 321 self._fin = fin
372 322 self._fout = fout
373 323
374 324 @property
375 325 def name(self):
376 326 return SSHV1
377 327
378 328 def getargs(self, args):
379 329 data = {}
380 330 keys = args.split()
381 331 for n in xrange(len(keys)):
382 332 argline = self._fin.readline()[:-1]
383 333 arg, l = argline.split()
384 334 if arg not in keys:
385 335 raise error.Abort(_("unexpected parameter %r") % arg)
386 336 if arg == '*':
387 337 star = {}
388 338 for k in xrange(int(l)):
389 339 argline = self._fin.readline()[:-1]
390 340 arg, l = argline.split()
391 341 val = self._fin.read(int(l))
392 342 star[arg] = val
393 343 data['*'] = star
394 344 else:
395 345 val = self._fin.read(int(l))
396 346 data[arg] = val
397 347 return [data[k] for k in keys]
398 348
399 349 def forwardpayload(self, fpout):
400 350 # The file is in the form:
401 351 #
402 352 # <chunk size>\n<chunk>
403 353 # ...
404 354 # 0\n
405 355 _sshv1respondbytes(self._fout, b'')
406 356 count = int(self._fin.readline())
407 357 while count:
408 358 fpout.write(self._fin.read(count))
409 359 count = int(self._fin.readline())
410 360
411 361 @contextlib.contextmanager
412 362 def mayberedirectstdio(self):
413 363 yield None
414 364
415 365 def client(self):
416 366 client = encoding.environ.get('SSH_CLIENT', '').split(' ', 1)[0]
417 367 return 'remote:ssh:' + client
418 368
419 369 class sshv2protocolhandler(sshv1protocolhandler):
420 370 """Protocol handler for version 2 of the SSH protocol."""
421 371
422 372 def _runsshserver(ui, repo, fin, fout):
423 373 # This function operates like a state machine of sorts. The following
424 374 # states are defined:
425 375 #
426 376 # protov1-serving
427 377 # Server is in protocol version 1 serving mode. Commands arrive on
428 378 # new lines. These commands are processed in this state, one command
429 379 # after the other.
430 380 #
431 381 # protov2-serving
432 382 # Server is in protocol version 2 serving mode.
433 383 #
434 384 # upgrade-initial
435 385 # The server is going to process an upgrade request.
436 386 #
437 387 # upgrade-v2-filter-legacy-handshake
438 388 # The protocol is being upgraded to version 2. The server is expecting
439 389 # the legacy handshake from version 1.
440 390 #
441 391 # upgrade-v2-finish
442 392 # The upgrade to version 2 of the protocol is imminent.
443 393 #
444 394 # shutdown
445 395 # The server is shutting down, possibly in reaction to a client event.
446 396 #
447 397 # And here are their transitions:
448 398 #
449 399 # protov1-serving -> shutdown
450 400 # When server receives an empty request or encounters another
451 401 # error.
452 402 #
453 403 # protov1-serving -> upgrade-initial
454 404 # An upgrade request line was seen.
455 405 #
456 406 # upgrade-initial -> upgrade-v2-filter-legacy-handshake
457 407 # Upgrade to version 2 in progress. Server is expecting to
458 408 # process a legacy handshake.
459 409 #
460 410 # upgrade-v2-filter-legacy-handshake -> shutdown
461 411 # Client did not fulfill upgrade handshake requirements.
462 412 #
463 413 # upgrade-v2-filter-legacy-handshake -> upgrade-v2-finish
464 414 # Client fulfilled version 2 upgrade requirements. Finishing that
465 415 # upgrade.
466 416 #
467 417 # upgrade-v2-finish -> protov2-serving
468 418 # Protocol upgrade to version 2 complete. Server can now speak protocol
469 419 # version 2.
470 420 #
471 421 # protov2-serving -> protov1-serving
472 422 # Ths happens by default since protocol version 2 is the same as
473 423 # version 1 except for the handshake.
474 424
475 425 state = 'protov1-serving'
476 426 proto = sshv1protocolhandler(ui, fin, fout)
477 427 protoswitched = False
478 428
479 429 while True:
480 430 if state == 'protov1-serving':
481 431 # Commands are issued on new lines.
482 432 request = fin.readline()[:-1]
483 433
484 434 # Empty lines signal to terminate the connection.
485 435 if not request:
486 436 state = 'shutdown'
487 437 continue
488 438
489 439 # It looks like a protocol upgrade request. Transition state to
490 440 # handle it.
491 441 if request.startswith(b'upgrade '):
492 442 if protoswitched:
493 443 _sshv1respondooberror(fout, ui.ferr,
494 444 b'cannot upgrade protocols multiple '
495 445 b'times')
496 446 state = 'shutdown'
497 447 continue
498 448
499 449 state = 'upgrade-initial'
500 450 continue
501 451
502 452 available = wireproto.commands.commandavailable(request, proto)
503 453
504 454 # This command isn't available. Send an empty response and go
505 455 # back to waiting for a new command.
506 456 if not available:
507 457 _sshv1respondbytes(fout, b'')
508 458 continue
509 459
510 460 rsp = wireproto.dispatch(repo, proto, request)
511 461
512 462 if isinstance(rsp, bytes):
513 463 _sshv1respondbytes(fout, rsp)
514 464 elif isinstance(rsp, wireprototypes.bytesresponse):
515 465 _sshv1respondbytes(fout, rsp.data)
516 466 elif isinstance(rsp, wireprototypes.streamres):
517 467 _sshv1respondstream(fout, rsp)
518 468 elif isinstance(rsp, wireprototypes.streamreslegacy):
519 469 _sshv1respondstream(fout, rsp)
520 470 elif isinstance(rsp, wireprototypes.pushres):
521 471 _sshv1respondbytes(fout, b'')
522 472 _sshv1respondbytes(fout, b'%d' % rsp.res)
523 473 elif isinstance(rsp, wireprototypes.pusherr):
524 474 _sshv1respondbytes(fout, rsp.res)
525 475 elif isinstance(rsp, wireprototypes.ooberror):
526 476 _sshv1respondooberror(fout, ui.ferr, rsp.message)
527 477 else:
528 478 raise error.ProgrammingError('unhandled response type from '
529 479 'wire protocol command: %s' % rsp)
530 480
531 481 # For now, protocol version 2 serving just goes back to version 1.
532 482 elif state == 'protov2-serving':
533 483 state = 'protov1-serving'
534 484 continue
535 485
536 486 elif state == 'upgrade-initial':
537 487 # We should never transition into this state if we've switched
538 488 # protocols.
539 489 assert not protoswitched
540 490 assert proto.name == SSHV1
541 491
542 492 # Expected: upgrade <token> <capabilities>
543 493 # If we get something else, the request is malformed. It could be
544 494 # from a future client that has altered the upgrade line content.
545 495 # We treat this as an unknown command.
546 496 try:
547 497 token, caps = request.split(b' ')[1:]
548 498 except ValueError:
549 499 _sshv1respondbytes(fout, b'')
550 500 state = 'protov1-serving'
551 501 continue
552 502
553 503 # Send empty response if we don't support upgrading protocols.
554 504 if not ui.configbool('experimental', 'sshserver.support-v2'):
555 505 _sshv1respondbytes(fout, b'')
556 506 state = 'protov1-serving'
557 507 continue
558 508
559 509 try:
560 510 caps = urlreq.parseqs(caps)
561 511 except ValueError:
562 512 _sshv1respondbytes(fout, b'')
563 513 state = 'protov1-serving'
564 514 continue
565 515
566 516 # We don't see an upgrade request to protocol version 2. Ignore
567 517 # the upgrade request.
568 518 wantedprotos = caps.get(b'proto', [b''])[0]
569 519 if SSHV2 not in wantedprotos:
570 520 _sshv1respondbytes(fout, b'')
571 521 state = 'protov1-serving'
572 522 continue
573 523
574 524 # It looks like we can honor this upgrade request to protocol 2.
575 525 # Filter the rest of the handshake protocol request lines.
576 526 state = 'upgrade-v2-filter-legacy-handshake'
577 527 continue
578 528
579 529 elif state == 'upgrade-v2-filter-legacy-handshake':
580 530 # Client should have sent legacy handshake after an ``upgrade``
581 531 # request. Expected lines:
582 532 #
583 533 # hello
584 534 # between
585 535 # pairs 81
586 536 # 0000...-0000...
587 537
588 538 ok = True
589 539 for line in (b'hello', b'between', b'pairs 81'):
590 540 request = fin.readline()[:-1]
591 541
592 542 if request != line:
593 543 _sshv1respondooberror(fout, ui.ferr,
594 544 b'malformed handshake protocol: '
595 545 b'missing %s' % line)
596 546 ok = False
597 547 state = 'shutdown'
598 548 break
599 549
600 550 if not ok:
601 551 continue
602 552
603 553 request = fin.read(81)
604 554 if request != b'%s-%s' % (b'0' * 40, b'0' * 40):
605 555 _sshv1respondooberror(fout, ui.ferr,
606 556 b'malformed handshake protocol: '
607 557 b'missing between argument value')
608 558 state = 'shutdown'
609 559 continue
610 560
611 561 state = 'upgrade-v2-finish'
612 562 continue
613 563
614 564 elif state == 'upgrade-v2-finish':
615 565 # Send the upgrade response.
616 566 fout.write(b'upgraded %s %s\n' % (token, SSHV2))
617 567 servercaps = wireproto.capabilities(repo, proto)
618 568 rsp = b'capabilities: %s' % servercaps.data
619 569 fout.write(b'%d\n%s\n' % (len(rsp), rsp))
620 570 fout.flush()
621 571
622 572 proto = sshv2protocolhandler(ui, fin, fout)
623 573 protoswitched = True
624 574
625 575 state = 'protov2-serving'
626 576 continue
627 577
628 578 elif state == 'shutdown':
629 579 break
630 580
631 581 else:
632 582 raise error.ProgrammingError('unhandled ssh server state: %s' %
633 583 state)
634 584
635 585 class sshserver(object):
636 586 def __init__(self, ui, repo):
637 587 self._ui = ui
638 588 self._repo = repo
639 589 self._fin = ui.fin
640 590 self._fout = ui.fout
641 591
642 592 hook.redirect(True)
643 593 ui.fout = repo.ui.fout = ui.ferr
644 594
645 595 # Prevent insertion/deletion of CRs
646 596 util.setbinary(self._fin)
647 597 util.setbinary(self._fout)
648 598
649 599 def serve_forever(self):
650 600 _runsshserver(self._ui, self._repo, self._fin, self._fout)
651 601 sys.exit(0)
@@ -1,66 +1,117 b''
1 1 # Copyright 2018 Gregory Szorc <gregory.szorc@gmail.com>
2 2 #
3 3 # This software may be used and distributed according to the terms of the
4 4 # GNU General Public License version 2 or any later version.
5 5
6 6 from __future__ import absolute_import
7 7
8 import abc
9
8 10 class bytesresponse(object):
9 11 """A wire protocol response consisting of raw bytes."""
10 12 def __init__(self, data):
11 13 self.data = data
12 14
13 15 class ooberror(object):
14 16 """wireproto reply: failure of a batch of operation
15 17
16 18 Something failed during a batch call. The error message is stored in
17 19 `self.message`.
18 20 """
19 21 def __init__(self, message):
20 22 self.message = message
21 23
22 24 class pushres(object):
23 25 """wireproto reply: success with simple integer return
24 26
25 27 The call was successful and returned an integer contained in `self.res`.
26 28 """
27 29 def __init__(self, res, output):
28 30 self.res = res
29 31 self.output = output
30 32
31 33 class pusherr(object):
32 34 """wireproto reply: failure
33 35
34 36 The call failed. The `self.res` attribute contains the error message.
35 37 """
36 38 def __init__(self, res, output):
37 39 self.res = res
38 40 self.output = output
39 41
40 42 class streamres(object):
41 43 """wireproto reply: binary stream
42 44
43 45 The call was successful and the result is a stream.
44 46
45 47 Accepts a generator containing chunks of data to be sent to the client.
46 48
47 49 ``prefer_uncompressed`` indicates that the data is expected to be
48 50 uncompressable and that the stream should therefore use the ``none``
49 51 engine.
50 52 """
51 53 def __init__(self, gen=None, prefer_uncompressed=False):
52 54 self.gen = gen
53 55 self.prefer_uncompressed = prefer_uncompressed
54 56
55 57 class streamreslegacy(object):
56 58 """wireproto reply: uncompressed binary stream
57 59
58 60 The call was successful and the result is a stream.
59 61
60 62 Accepts a generator containing chunks of data to be sent to the client.
61 63
62 64 Like ``streamres``, but sends an uncompressed data for "version 1" clients
63 65 using the application/mercurial-0.1 media type.
64 66 """
65 67 def __init__(self, gen=None):
66 68 self.gen = gen
69
70 class baseprotocolhandler(object):
71 """Abstract base class for wire protocol handlers.
72
73 A wire protocol handler serves as an interface between protocol command
74 handlers and the wire protocol transport layer. Protocol handlers provide
75 methods to read command arguments, redirect stdio for the duration of
76 the request, handle response types, etc.
77 """
78
79 __metaclass__ = abc.ABCMeta
80
81 @abc.abstractproperty
82 def name(self):
83 """The name of the protocol implementation.
84
85 Used for uniquely identifying the transport type.
86 """
87
88 @abc.abstractmethod
89 def getargs(self, args):
90 """return the value for arguments in <args>
91
92 returns a list of values (same order as <args>)"""
93
94 @abc.abstractmethod
95 def forwardpayload(self, fp):
96 """Read the raw payload and forward to a file.
97
98 The payload is read in full before the function returns.
99 """
100
101 @abc.abstractmethod
102 def mayberedirectstdio(self):
103 """Context manager to possibly redirect stdio.
104
105 The context manager yields a file-object like object that receives
106 stdout and stderr output when the context manager is active. Or it
107 yields ``None`` if no I/O redirection occurs.
108
109 The intent of this context manager is to capture stdio output
110 so it may be sent in the response. Some transports support streaming
111 stdio to the client in real time. For these transports, stdio output
112 won't be captured.
113 """
114
115 @abc.abstractmethod
116 def client(self):
117 """Returns a string representation of this client (as bytes)."""
General Comments 0
You need to be logged in to leave comments. Login now