##// END OF EJS Templates
wireprotoframing: use our CBOR module...
Gregory Szorc -
r39477:36f487a3 default
parent child Browse files
Show More
@@ -1,1167 +1,1167 b''
1 1 # wireprotoframing.py - unified framing protocol for wire protocol
2 2 #
3 3 # Copyright 2018 Gregory Szorc <gregory.szorc@gmail.com>
4 4 #
5 5 # This software may be used and distributed according to the terms of the
6 6 # GNU General Public License version 2 or any later version.
7 7
8 8 # This file contains functionality to support the unified frame-based wire
9 9 # protocol. For details about the protocol, see
10 10 # `hg help internals.wireprotocol`.
11 11
12 12 from __future__ import absolute_import
13 13
14 14 import collections
15 15 import struct
16 16
17 17 from .i18n import _
18 18 from .thirdparty import (
19 19 attr,
20 cbor,
21 20 )
22 21 from . import (
23 22 encoding,
24 23 error,
25 24 util,
26 25 )
27 26 from .utils import (
27 cborutil,
28 28 stringutil,
29 29 )
30 30
31 31 FRAME_HEADER_SIZE = 8
32 32 DEFAULT_MAX_FRAME_SIZE = 32768
33 33
34 34 STREAM_FLAG_BEGIN_STREAM = 0x01
35 35 STREAM_FLAG_END_STREAM = 0x02
36 36 STREAM_FLAG_ENCODING_APPLIED = 0x04
37 37
38 38 STREAM_FLAGS = {
39 39 b'stream-begin': STREAM_FLAG_BEGIN_STREAM,
40 40 b'stream-end': STREAM_FLAG_END_STREAM,
41 41 b'encoded': STREAM_FLAG_ENCODING_APPLIED,
42 42 }
43 43
44 44 FRAME_TYPE_COMMAND_REQUEST = 0x01
45 45 FRAME_TYPE_COMMAND_DATA = 0x02
46 46 FRAME_TYPE_COMMAND_RESPONSE = 0x03
47 47 FRAME_TYPE_ERROR_RESPONSE = 0x05
48 48 FRAME_TYPE_TEXT_OUTPUT = 0x06
49 49 FRAME_TYPE_PROGRESS = 0x07
50 50 FRAME_TYPE_STREAM_SETTINGS = 0x08
51 51
52 52 FRAME_TYPES = {
53 53 b'command-request': FRAME_TYPE_COMMAND_REQUEST,
54 54 b'command-data': FRAME_TYPE_COMMAND_DATA,
55 55 b'command-response': FRAME_TYPE_COMMAND_RESPONSE,
56 56 b'error-response': FRAME_TYPE_ERROR_RESPONSE,
57 57 b'text-output': FRAME_TYPE_TEXT_OUTPUT,
58 58 b'progress': FRAME_TYPE_PROGRESS,
59 59 b'stream-settings': FRAME_TYPE_STREAM_SETTINGS,
60 60 }
61 61
62 62 FLAG_COMMAND_REQUEST_NEW = 0x01
63 63 FLAG_COMMAND_REQUEST_CONTINUATION = 0x02
64 64 FLAG_COMMAND_REQUEST_MORE_FRAMES = 0x04
65 65 FLAG_COMMAND_REQUEST_EXPECT_DATA = 0x08
66 66
67 67 FLAGS_COMMAND_REQUEST = {
68 68 b'new': FLAG_COMMAND_REQUEST_NEW,
69 69 b'continuation': FLAG_COMMAND_REQUEST_CONTINUATION,
70 70 b'more': FLAG_COMMAND_REQUEST_MORE_FRAMES,
71 71 b'have-data': FLAG_COMMAND_REQUEST_EXPECT_DATA,
72 72 }
73 73
74 74 FLAG_COMMAND_DATA_CONTINUATION = 0x01
75 75 FLAG_COMMAND_DATA_EOS = 0x02
76 76
77 77 FLAGS_COMMAND_DATA = {
78 78 b'continuation': FLAG_COMMAND_DATA_CONTINUATION,
79 79 b'eos': FLAG_COMMAND_DATA_EOS,
80 80 }
81 81
82 82 FLAG_COMMAND_RESPONSE_CONTINUATION = 0x01
83 83 FLAG_COMMAND_RESPONSE_EOS = 0x02
84 84
85 85 FLAGS_COMMAND_RESPONSE = {
86 86 b'continuation': FLAG_COMMAND_RESPONSE_CONTINUATION,
87 87 b'eos': FLAG_COMMAND_RESPONSE_EOS,
88 88 }
89 89
90 90 # Maps frame types to their available flags.
91 91 FRAME_TYPE_FLAGS = {
92 92 FRAME_TYPE_COMMAND_REQUEST: FLAGS_COMMAND_REQUEST,
93 93 FRAME_TYPE_COMMAND_DATA: FLAGS_COMMAND_DATA,
94 94 FRAME_TYPE_COMMAND_RESPONSE: FLAGS_COMMAND_RESPONSE,
95 95 FRAME_TYPE_ERROR_RESPONSE: {},
96 96 FRAME_TYPE_TEXT_OUTPUT: {},
97 97 FRAME_TYPE_PROGRESS: {},
98 98 FRAME_TYPE_STREAM_SETTINGS: {},
99 99 }
100 100
101 101 ARGUMENT_RECORD_HEADER = struct.Struct(r'<HH')
102 102
103 103 def humanflags(mapping, value):
104 104 """Convert a numeric flags value to a human value, using a mapping table."""
105 105 namemap = {v: k for k, v in mapping.iteritems()}
106 106 flags = []
107 107 val = 1
108 108 while value >= val:
109 109 if value & val:
110 110 flags.append(namemap.get(val, '<unknown 0x%02x>' % val))
111 111 val <<= 1
112 112
113 113 return b'|'.join(flags)
114 114
115 115 @attr.s(slots=True)
116 116 class frameheader(object):
117 117 """Represents the data in a frame header."""
118 118
119 119 length = attr.ib()
120 120 requestid = attr.ib()
121 121 streamid = attr.ib()
122 122 streamflags = attr.ib()
123 123 typeid = attr.ib()
124 124 flags = attr.ib()
125 125
126 126 @attr.s(slots=True, repr=False)
127 127 class frame(object):
128 128 """Represents a parsed frame."""
129 129
130 130 requestid = attr.ib()
131 131 streamid = attr.ib()
132 132 streamflags = attr.ib()
133 133 typeid = attr.ib()
134 134 flags = attr.ib()
135 135 payload = attr.ib()
136 136
137 137 @encoding.strmethod
138 138 def __repr__(self):
139 139 typename = '<unknown 0x%02x>' % self.typeid
140 140 for name, value in FRAME_TYPES.iteritems():
141 141 if value == self.typeid:
142 142 typename = name
143 143 break
144 144
145 145 return ('frame(size=%d; request=%d; stream=%d; streamflags=%s; '
146 146 'type=%s; flags=%s)' % (
147 147 len(self.payload), self.requestid, self.streamid,
148 148 humanflags(STREAM_FLAGS, self.streamflags), typename,
149 149 humanflags(FRAME_TYPE_FLAGS.get(self.typeid, {}), self.flags)))
150 150
151 151 def makeframe(requestid, streamid, streamflags, typeid, flags, payload):
152 152 """Assemble a frame into a byte array."""
153 153 # TODO assert size of payload.
154 154 frame = bytearray(FRAME_HEADER_SIZE + len(payload))
155 155
156 156 # 24 bits length
157 157 # 16 bits request id
158 158 # 8 bits stream id
159 159 # 8 bits stream flags
160 160 # 4 bits type
161 161 # 4 bits flags
162 162
163 163 l = struct.pack(r'<I', len(payload))
164 164 frame[0:3] = l[0:3]
165 165 struct.pack_into(r'<HBB', frame, 3, requestid, streamid, streamflags)
166 166 frame[7] = (typeid << 4) | flags
167 167 frame[8:] = payload
168 168
169 169 return frame
170 170
171 171 def makeframefromhumanstring(s):
172 172 """Create a frame from a human readable string
173 173
174 174 Strings have the form:
175 175
176 176 <request-id> <stream-id> <stream-flags> <type> <flags> <payload>
177 177
178 178 This can be used by user-facing applications and tests for creating
179 179 frames easily without having to type out a bunch of constants.
180 180
181 181 Request ID and stream IDs are integers.
182 182
183 183 Stream flags, frame type, and flags can be specified by integer or
184 184 named constant.
185 185
186 186 Flags can be delimited by `|` to bitwise OR them together.
187 187
188 188 If the payload begins with ``cbor:``, the following string will be
189 189 evaluated as Python literal and the resulting object will be fed into
190 190 a CBOR encoder. Otherwise, the payload is interpreted as a Python
191 191 byte string literal.
192 192 """
193 193 fields = s.split(b' ', 5)
194 194 requestid, streamid, streamflags, frametype, frameflags, payload = fields
195 195
196 196 requestid = int(requestid)
197 197 streamid = int(streamid)
198 198
199 199 finalstreamflags = 0
200 200 for flag in streamflags.split(b'|'):
201 201 if flag in STREAM_FLAGS:
202 202 finalstreamflags |= STREAM_FLAGS[flag]
203 203 else:
204 204 finalstreamflags |= int(flag)
205 205
206 206 if frametype in FRAME_TYPES:
207 207 frametype = FRAME_TYPES[frametype]
208 208 else:
209 209 frametype = int(frametype)
210 210
211 211 finalflags = 0
212 212 validflags = FRAME_TYPE_FLAGS[frametype]
213 213 for flag in frameflags.split(b'|'):
214 214 if flag in validflags:
215 215 finalflags |= validflags[flag]
216 216 else:
217 217 finalflags |= int(flag)
218 218
219 219 if payload.startswith(b'cbor:'):
220 payload = cbor.dumps(stringutil.evalpythonliteral(payload[5:]),
221 canonical=True)
220 payload = b''.join(cborutil.streamencode(
221 stringutil.evalpythonliteral(payload[5:])))
222 222
223 223 else:
224 224 payload = stringutil.unescapestr(payload)
225 225
226 226 return makeframe(requestid=requestid, streamid=streamid,
227 227 streamflags=finalstreamflags, typeid=frametype,
228 228 flags=finalflags, payload=payload)
229 229
230 230 def parseheader(data):
231 231 """Parse a unified framing protocol frame header from a buffer.
232 232
233 233 The header is expected to be in the buffer at offset 0 and the
234 234 buffer is expected to be large enough to hold a full header.
235 235 """
236 236 # 24 bits payload length (little endian)
237 237 # 16 bits request ID
238 238 # 8 bits stream ID
239 239 # 8 bits stream flags
240 240 # 4 bits frame type
241 241 # 4 bits frame flags
242 242 # ... payload
243 243 framelength = data[0] + 256 * data[1] + 16384 * data[2]
244 244 requestid, streamid, streamflags = struct.unpack_from(r'<HBB', data, 3)
245 245 typeflags = data[7]
246 246
247 247 frametype = (typeflags & 0xf0) >> 4
248 248 frameflags = typeflags & 0x0f
249 249
250 250 return frameheader(framelength, requestid, streamid, streamflags,
251 251 frametype, frameflags)
252 252
253 253 def readframe(fh):
254 254 """Read a unified framing protocol frame from a file object.
255 255
256 256 Returns a 3-tuple of (type, flags, payload) for the decoded frame or
257 257 None if no frame is available. May raise if a malformed frame is
258 258 seen.
259 259 """
260 260 header = bytearray(FRAME_HEADER_SIZE)
261 261
262 262 readcount = fh.readinto(header)
263 263
264 264 if readcount == 0:
265 265 return None
266 266
267 267 if readcount != FRAME_HEADER_SIZE:
268 268 raise error.Abort(_('received incomplete frame: got %d bytes: %s') %
269 269 (readcount, header))
270 270
271 271 h = parseheader(header)
272 272
273 273 payload = fh.read(h.length)
274 274 if len(payload) != h.length:
275 275 raise error.Abort(_('frame length error: expected %d; got %d') %
276 276 (h.length, len(payload)))
277 277
278 278 return frame(h.requestid, h.streamid, h.streamflags, h.typeid, h.flags,
279 279 payload)
280 280
281 281 def createcommandframes(stream, requestid, cmd, args, datafh=None,
282 282 maxframesize=DEFAULT_MAX_FRAME_SIZE):
283 283 """Create frames necessary to transmit a request to run a command.
284 284
285 285 This is a generator of bytearrays. Each item represents a frame
286 286 ready to be sent over the wire to a peer.
287 287 """
288 288 data = {b'name': cmd}
289 289 if args:
290 290 data[b'args'] = args
291 291
292 data = cbor.dumps(data, canonical=True)
292 data = b''.join(cborutil.streamencode(data))
293 293
294 294 offset = 0
295 295
296 296 while True:
297 297 flags = 0
298 298
299 299 # Must set new or continuation flag.
300 300 if not offset:
301 301 flags |= FLAG_COMMAND_REQUEST_NEW
302 302 else:
303 303 flags |= FLAG_COMMAND_REQUEST_CONTINUATION
304 304
305 305 # Data frames is set on all frames.
306 306 if datafh:
307 307 flags |= FLAG_COMMAND_REQUEST_EXPECT_DATA
308 308
309 309 payload = data[offset:offset + maxframesize]
310 310 offset += len(payload)
311 311
312 312 if len(payload) == maxframesize and offset < len(data):
313 313 flags |= FLAG_COMMAND_REQUEST_MORE_FRAMES
314 314
315 315 yield stream.makeframe(requestid=requestid,
316 316 typeid=FRAME_TYPE_COMMAND_REQUEST,
317 317 flags=flags,
318 318 payload=payload)
319 319
320 320 if not (flags & FLAG_COMMAND_REQUEST_MORE_FRAMES):
321 321 break
322 322
323 323 if datafh:
324 324 while True:
325 325 data = datafh.read(DEFAULT_MAX_FRAME_SIZE)
326 326
327 327 done = False
328 328 if len(data) == DEFAULT_MAX_FRAME_SIZE:
329 329 flags = FLAG_COMMAND_DATA_CONTINUATION
330 330 else:
331 331 flags = FLAG_COMMAND_DATA_EOS
332 332 assert datafh.read(1) == b''
333 333 done = True
334 334
335 335 yield stream.makeframe(requestid=requestid,
336 336 typeid=FRAME_TYPE_COMMAND_DATA,
337 337 flags=flags,
338 338 payload=data)
339 339
340 340 if done:
341 341 break
342 342
343 343 def createcommandresponseframesfrombytes(stream, requestid, data,
344 344 maxframesize=DEFAULT_MAX_FRAME_SIZE):
345 345 """Create a raw frame to send a bytes response from static bytes input.
346 346
347 347 Returns a generator of bytearrays.
348 348 """
349 349 # Automatically send the overall CBOR response map.
350 overall = cbor.dumps({b'status': b'ok'}, canonical=True)
350 overall = b''.join(cborutil.streamencode({b'status': b'ok'}))
351 351 if len(overall) > maxframesize:
352 352 raise error.ProgrammingError('not yet implemented')
353 353
354 354 # Simple case where we can fit the full response in a single frame.
355 355 if len(overall) + len(data) <= maxframesize:
356 356 flags = FLAG_COMMAND_RESPONSE_EOS
357 357 yield stream.makeframe(requestid=requestid,
358 358 typeid=FRAME_TYPE_COMMAND_RESPONSE,
359 359 flags=flags,
360 360 payload=overall + data)
361 361 return
362 362
363 363 # It's easier to send the overall CBOR map in its own frame than to track
364 364 # offsets.
365 365 yield stream.makeframe(requestid=requestid,
366 366 typeid=FRAME_TYPE_COMMAND_RESPONSE,
367 367 flags=FLAG_COMMAND_RESPONSE_CONTINUATION,
368 368 payload=overall)
369 369
370 370 offset = 0
371 371 while True:
372 372 chunk = data[offset:offset + maxframesize]
373 373 offset += len(chunk)
374 374 done = offset == len(data)
375 375
376 376 if done:
377 377 flags = FLAG_COMMAND_RESPONSE_EOS
378 378 else:
379 379 flags = FLAG_COMMAND_RESPONSE_CONTINUATION
380 380
381 381 yield stream.makeframe(requestid=requestid,
382 382 typeid=FRAME_TYPE_COMMAND_RESPONSE,
383 383 flags=flags,
384 384 payload=chunk)
385 385
386 386 if done:
387 387 break
388 388
389 389 def createbytesresponseframesfromgen(stream, requestid, gen,
390 390 maxframesize=DEFAULT_MAX_FRAME_SIZE):
391 overall = cbor.dumps({b'status': b'ok'}, canonical=True)
391 overall = b''.join(cborutil.streamencode({b'status': b'ok'}))
392 392
393 393 yield stream.makeframe(requestid=requestid,
394 394 typeid=FRAME_TYPE_COMMAND_RESPONSE,
395 395 flags=FLAG_COMMAND_RESPONSE_CONTINUATION,
396 396 payload=overall)
397 397
398 398 cb = util.chunkbuffer(gen)
399 399
400 400 flags = 0
401 401
402 402 while True:
403 403 chunk = cb.read(maxframesize)
404 404 if not chunk:
405 405 break
406 406
407 407 yield stream.makeframe(requestid=requestid,
408 408 typeid=FRAME_TYPE_COMMAND_RESPONSE,
409 409 flags=flags,
410 410 payload=chunk)
411 411
412 412 flags |= FLAG_COMMAND_RESPONSE_CONTINUATION
413 413
414 414 flags ^= FLAG_COMMAND_RESPONSE_CONTINUATION
415 415 flags |= FLAG_COMMAND_RESPONSE_EOS
416 416 yield stream.makeframe(requestid=requestid,
417 417 typeid=FRAME_TYPE_COMMAND_RESPONSE,
418 418 flags=flags,
419 419 payload=b'')
420 420
421 421 def createcommanderrorresponse(stream, requestid, message, args=None):
422 422 m = {
423 423 b'status': b'error',
424 424 b'error': {
425 425 b'message': message,
426 426 }
427 427 }
428 428
429 429 if args:
430 430 m[b'error'][b'args'] = args
431 431
432 overall = cbor.dumps(m, canonical=True)
432 overall = b''.join(cborutil.streamencode(m))
433 433
434 434 yield stream.makeframe(requestid=requestid,
435 435 typeid=FRAME_TYPE_COMMAND_RESPONSE,
436 436 flags=FLAG_COMMAND_RESPONSE_EOS,
437 437 payload=overall)
438 438
439 439 def createerrorframe(stream, requestid, msg, errtype):
440 440 # TODO properly handle frame size limits.
441 441 assert len(msg) <= DEFAULT_MAX_FRAME_SIZE
442 442
443 payload = cbor.dumps({
443 payload = b''.join(cborutil.streamencode({
444 444 b'type': errtype,
445 445 b'message': [{b'msg': msg}],
446 }, canonical=True)
446 }))
447 447
448 448 yield stream.makeframe(requestid=requestid,
449 449 typeid=FRAME_TYPE_ERROR_RESPONSE,
450 450 flags=0,
451 451 payload=payload)
452 452
453 453 def createtextoutputframe(stream, requestid, atoms,
454 454 maxframesize=DEFAULT_MAX_FRAME_SIZE):
455 455 """Create a text output frame to render text to people.
456 456
457 457 ``atoms`` is a 3-tuple of (formatting string, args, labels).
458 458
459 459 The formatting string contains ``%s`` tokens to be replaced by the
460 460 corresponding indexed entry in ``args``. ``labels`` is an iterable of
461 461 formatters to be applied at rendering time. In terms of the ``ui``
462 462 class, each atom corresponds to a ``ui.write()``.
463 463 """
464 464 atomdicts = []
465 465
466 466 for (formatting, args, labels) in atoms:
467 467 # TODO look for localstr, other types here?
468 468
469 469 if not isinstance(formatting, bytes):
470 470 raise ValueError('must use bytes formatting strings')
471 471 for arg in args:
472 472 if not isinstance(arg, bytes):
473 473 raise ValueError('must use bytes for arguments')
474 474 for label in labels:
475 475 if not isinstance(label, bytes):
476 476 raise ValueError('must use bytes for labels')
477 477
478 478 # Formatting string must be ASCII.
479 479 formatting = formatting.decode(r'ascii', r'replace').encode(r'ascii')
480 480
481 481 # Arguments must be UTF-8.
482 482 args = [a.decode(r'utf-8', r'replace').encode(r'utf-8') for a in args]
483 483
484 484 # Labels must be ASCII.
485 485 labels = [l.decode(r'ascii', r'strict').encode(r'ascii')
486 486 for l in labels]
487 487
488 488 atom = {b'msg': formatting}
489 489 if args:
490 490 atom[b'args'] = args
491 491 if labels:
492 492 atom[b'labels'] = labels
493 493
494 494 atomdicts.append(atom)
495 495
496 payload = cbor.dumps(atomdicts, canonical=True)
496 payload = b''.join(cborutil.streamencode(atomdicts))
497 497
498 498 if len(payload) > maxframesize:
499 499 raise ValueError('cannot encode data in a single frame')
500 500
501 501 yield stream.makeframe(requestid=requestid,
502 502 typeid=FRAME_TYPE_TEXT_OUTPUT,
503 503 flags=0,
504 504 payload=payload)
505 505
506 506 class stream(object):
507 507 """Represents a logical unidirectional series of frames."""
508 508
509 509 def __init__(self, streamid, active=False):
510 510 self.streamid = streamid
511 511 self._active = active
512 512
513 513 def makeframe(self, requestid, typeid, flags, payload):
514 514 """Create a frame to be sent out over this stream.
515 515
516 516 Only returns the frame instance. Does not actually send it.
517 517 """
518 518 streamflags = 0
519 519 if not self._active:
520 520 streamflags |= STREAM_FLAG_BEGIN_STREAM
521 521 self._active = True
522 522
523 523 return makeframe(requestid, self.streamid, streamflags, typeid, flags,
524 524 payload)
525 525
526 526 def ensureserverstream(stream):
527 527 if stream.streamid % 2:
528 528 raise error.ProgrammingError('server should only write to even '
529 529 'numbered streams; %d is not even' %
530 530 stream.streamid)
531 531
532 532 class serverreactor(object):
533 533 """Holds state of a server handling frame-based protocol requests.
534 534
535 535 This class is the "brain" of the unified frame-based protocol server
536 536 component. While the protocol is stateless from the perspective of
537 537 requests/commands, something needs to track which frames have been
538 538 received, what frames to expect, etc. This class is that thing.
539 539
540 540 Instances are modeled as a state machine of sorts. Instances are also
541 541 reactionary to external events. The point of this class is to encapsulate
542 542 the state of the connection and the exchange of frames, not to perform
543 543 work. Instead, callers tell this class when something occurs, like a
544 544 frame arriving. If that activity is worthy of a follow-up action (say
545 545 *run a command*), the return value of that handler will say so.
546 546
547 547 I/O and CPU intensive operations are purposefully delegated outside of
548 548 this class.
549 549
550 550 Consumers are expected to tell instances when events occur. They do so by
551 551 calling the various ``on*`` methods. These methods return a 2-tuple
552 552 describing any follow-up action(s) to take. The first element is the
553 553 name of an action to perform. The second is a data structure (usually
554 554 a dict) specific to that action that contains more information. e.g.
555 555 if the server wants to send frames back to the client, the data structure
556 556 will contain a reference to those frames.
557 557
558 558 Valid actions that consumers can be instructed to take are:
559 559
560 560 sendframes
561 561 Indicates that frames should be sent to the client. The ``framegen``
562 562 key contains a generator of frames that should be sent. The server
563 563 assumes that all frames are sent to the client.
564 564
565 565 error
566 566 Indicates that an error occurred. Consumer should probably abort.
567 567
568 568 runcommand
569 569 Indicates that the consumer should run a wire protocol command. Details
570 570 of the command to run are given in the data structure.
571 571
572 572 wantframe
573 573 Indicates that nothing of interest happened and the server is waiting on
574 574 more frames from the client before anything interesting can be done.
575 575
576 576 noop
577 577 Indicates no additional action is required.
578 578
579 579 Known Issues
580 580 ------------
581 581
582 582 There are no limits to the number of partially received commands or their
583 583 size. A malicious client could stream command request data and exhaust the
584 584 server's memory.
585 585
586 586 Partially received commands are not acted upon when end of input is
587 587 reached. Should the server error if it receives a partial request?
588 588 Should the client send a message to abort a partially transmitted request
589 589 to facilitate graceful shutdown?
590 590
591 591 Active requests that haven't been responded to aren't tracked. This means
592 592 that if we receive a command and instruct its dispatch, another command
593 593 with its request ID can come in over the wire and there will be a race
594 594 between who responds to what.
595 595 """
596 596
597 597 def __init__(self, deferoutput=False):
598 598 """Construct a new server reactor.
599 599
600 600 ``deferoutput`` can be used to indicate that no output frames should be
601 601 instructed to be sent until input has been exhausted. In this mode,
602 602 events that would normally generate output frames (such as a command
603 603 response being ready) will instead defer instructing the consumer to
604 604 send those frames. This is useful for half-duplex transports where the
605 605 sender cannot receive until all data has been transmitted.
606 606 """
607 607 self._deferoutput = deferoutput
608 608 self._state = 'idle'
609 609 self._nextoutgoingstreamid = 2
610 610 self._bufferedframegens = []
611 611 # stream id -> stream instance for all active streams from the client.
612 612 self._incomingstreams = {}
613 613 self._outgoingstreams = {}
614 614 # request id -> dict of commands that are actively being received.
615 615 self._receivingcommands = {}
616 616 # Request IDs that have been received and are actively being processed.
617 617 # Once all output for a request has been sent, it is removed from this
618 618 # set.
619 619 self._activecommands = set()
620 620
621 621 def onframerecv(self, frame):
622 622 """Process a frame that has been received off the wire.
623 623
624 624 Returns a dict with an ``action`` key that details what action,
625 625 if any, the consumer should take next.
626 626 """
627 627 if not frame.streamid % 2:
628 628 self._state = 'errored'
629 629 return self._makeerrorresult(
630 630 _('received frame with even numbered stream ID: %d') %
631 631 frame.streamid)
632 632
633 633 if frame.streamid not in self._incomingstreams:
634 634 if not frame.streamflags & STREAM_FLAG_BEGIN_STREAM:
635 635 self._state = 'errored'
636 636 return self._makeerrorresult(
637 637 _('received frame on unknown inactive stream without '
638 638 'beginning of stream flag set'))
639 639
640 640 self._incomingstreams[frame.streamid] = stream(frame.streamid)
641 641
642 642 if frame.streamflags & STREAM_FLAG_ENCODING_APPLIED:
643 643 # TODO handle decoding frames
644 644 self._state = 'errored'
645 645 raise error.ProgrammingError('support for decoding stream payloads '
646 646 'not yet implemented')
647 647
648 648 if frame.streamflags & STREAM_FLAG_END_STREAM:
649 649 del self._incomingstreams[frame.streamid]
650 650
651 651 handlers = {
652 652 'idle': self._onframeidle,
653 653 'command-receiving': self._onframecommandreceiving,
654 654 'errored': self._onframeerrored,
655 655 }
656 656
657 657 meth = handlers.get(self._state)
658 658 if not meth:
659 659 raise error.ProgrammingError('unhandled state: %s' % self._state)
660 660
661 661 return meth(frame)
662 662
663 663 def oncommandresponseready(self, stream, requestid, data):
664 664 """Signal that a bytes response is ready to be sent to the client.
665 665
666 666 The raw bytes response is passed as an argument.
667 667 """
668 668 ensureserverstream(stream)
669 669
670 670 def sendframes():
671 671 for frame in createcommandresponseframesfrombytes(stream, requestid,
672 672 data):
673 673 yield frame
674 674
675 675 self._activecommands.remove(requestid)
676 676
677 677 result = sendframes()
678 678
679 679 if self._deferoutput:
680 680 self._bufferedframegens.append(result)
681 681 return 'noop', {}
682 682 else:
683 683 return 'sendframes', {
684 684 'framegen': result,
685 685 }
686 686
687 687 def oncommandresponsereadygen(self, stream, requestid, gen):
688 688 """Signal that a bytes response is ready, with data as a generator."""
689 689 ensureserverstream(stream)
690 690
691 691 def sendframes():
692 692 for frame in createbytesresponseframesfromgen(stream, requestid,
693 693 gen):
694 694 yield frame
695 695
696 696 self._activecommands.remove(requestid)
697 697
698 698 return self._handlesendframes(sendframes())
699 699
700 700 def oninputeof(self):
701 701 """Signals that end of input has been received.
702 702
703 703 No more frames will be received. All pending activity should be
704 704 completed.
705 705 """
706 706 # TODO should we do anything about in-flight commands?
707 707
708 708 if not self._deferoutput or not self._bufferedframegens:
709 709 return 'noop', {}
710 710
711 711 # If we buffered all our responses, emit those.
712 712 def makegen():
713 713 for gen in self._bufferedframegens:
714 714 for frame in gen:
715 715 yield frame
716 716
717 717 return 'sendframes', {
718 718 'framegen': makegen(),
719 719 }
720 720
721 721 def _handlesendframes(self, framegen):
722 722 if self._deferoutput:
723 723 self._bufferedframegens.append(framegen)
724 724 return 'noop', {}
725 725 else:
726 726 return 'sendframes', {
727 727 'framegen': framegen,
728 728 }
729 729
730 730 def onservererror(self, stream, requestid, msg):
731 731 ensureserverstream(stream)
732 732
733 733 def sendframes():
734 734 for frame in createerrorframe(stream, requestid, msg,
735 735 errtype='server'):
736 736 yield frame
737 737
738 738 self._activecommands.remove(requestid)
739 739
740 740 return self._handlesendframes(sendframes())
741 741
742 742 def oncommanderror(self, stream, requestid, message, args=None):
743 743 """Called when a command encountered an error before sending output."""
744 744 ensureserverstream(stream)
745 745
746 746 def sendframes():
747 747 for frame in createcommanderrorresponse(stream, requestid, message,
748 748 args):
749 749 yield frame
750 750
751 751 self._activecommands.remove(requestid)
752 752
753 753 return self._handlesendframes(sendframes())
754 754
755 755 def makeoutputstream(self):
756 756 """Create a stream to be used for sending data to the client."""
757 757 streamid = self._nextoutgoingstreamid
758 758 self._nextoutgoingstreamid += 2
759 759
760 760 s = stream(streamid)
761 761 self._outgoingstreams[streamid] = s
762 762
763 763 return s
764 764
765 765 def _makeerrorresult(self, msg):
766 766 return 'error', {
767 767 'message': msg,
768 768 }
769 769
770 770 def _makeruncommandresult(self, requestid):
771 771 entry = self._receivingcommands[requestid]
772 772
773 773 if not entry['requestdone']:
774 774 self._state = 'errored'
775 775 raise error.ProgrammingError('should not be called without '
776 776 'requestdone set')
777 777
778 778 del self._receivingcommands[requestid]
779 779
780 780 if self._receivingcommands:
781 781 self._state = 'command-receiving'
782 782 else:
783 783 self._state = 'idle'
784 784
785 785 # Decode the payloads as CBOR.
786 786 entry['payload'].seek(0)
787 request = cbor.load(entry['payload'])
787 request = cborutil.decodeall(entry['payload'].getvalue())[0]
788 788
789 789 if b'name' not in request:
790 790 self._state = 'errored'
791 791 return self._makeerrorresult(
792 792 _('command request missing "name" field'))
793 793
794 794 if b'args' not in request:
795 795 request[b'args'] = {}
796 796
797 797 assert requestid not in self._activecommands
798 798 self._activecommands.add(requestid)
799 799
800 800 return 'runcommand', {
801 801 'requestid': requestid,
802 802 'command': request[b'name'],
803 803 'args': request[b'args'],
804 804 'data': entry['data'].getvalue() if entry['data'] else None,
805 805 }
806 806
807 807 def _makewantframeresult(self):
808 808 return 'wantframe', {
809 809 'state': self._state,
810 810 }
811 811
812 812 def _validatecommandrequestframe(self, frame):
813 813 new = frame.flags & FLAG_COMMAND_REQUEST_NEW
814 814 continuation = frame.flags & FLAG_COMMAND_REQUEST_CONTINUATION
815 815
816 816 if new and continuation:
817 817 self._state = 'errored'
818 818 return self._makeerrorresult(
819 819 _('received command request frame with both new and '
820 820 'continuation flags set'))
821 821
822 822 if not new and not continuation:
823 823 self._state = 'errored'
824 824 return self._makeerrorresult(
825 825 _('received command request frame with neither new nor '
826 826 'continuation flags set'))
827 827
828 828 def _onframeidle(self, frame):
829 829 # The only frame type that should be received in this state is a
830 830 # command request.
831 831 if frame.typeid != FRAME_TYPE_COMMAND_REQUEST:
832 832 self._state = 'errored'
833 833 return self._makeerrorresult(
834 834 _('expected command request frame; got %d') % frame.typeid)
835 835
836 836 res = self._validatecommandrequestframe(frame)
837 837 if res:
838 838 return res
839 839
840 840 if frame.requestid in self._receivingcommands:
841 841 self._state = 'errored'
842 842 return self._makeerrorresult(
843 843 _('request with ID %d already received') % frame.requestid)
844 844
845 845 if frame.requestid in self._activecommands:
846 846 self._state = 'errored'
847 847 return self._makeerrorresult(
848 848 _('request with ID %d is already active') % frame.requestid)
849 849
850 850 new = frame.flags & FLAG_COMMAND_REQUEST_NEW
851 851 moreframes = frame.flags & FLAG_COMMAND_REQUEST_MORE_FRAMES
852 852 expectingdata = frame.flags & FLAG_COMMAND_REQUEST_EXPECT_DATA
853 853
854 854 if not new:
855 855 self._state = 'errored'
856 856 return self._makeerrorresult(
857 857 _('received command request frame without new flag set'))
858 858
859 859 payload = util.bytesio()
860 860 payload.write(frame.payload)
861 861
862 862 self._receivingcommands[frame.requestid] = {
863 863 'payload': payload,
864 864 'data': None,
865 865 'requestdone': not moreframes,
866 866 'expectingdata': bool(expectingdata),
867 867 }
868 868
869 869 # This is the final frame for this request. Dispatch it.
870 870 if not moreframes and not expectingdata:
871 871 return self._makeruncommandresult(frame.requestid)
872 872
873 873 assert moreframes or expectingdata
874 874 self._state = 'command-receiving'
875 875 return self._makewantframeresult()
876 876
877 877 def _onframecommandreceiving(self, frame):
878 878 if frame.typeid == FRAME_TYPE_COMMAND_REQUEST:
879 879 # Process new command requests as such.
880 880 if frame.flags & FLAG_COMMAND_REQUEST_NEW:
881 881 return self._onframeidle(frame)
882 882
883 883 res = self._validatecommandrequestframe(frame)
884 884 if res:
885 885 return res
886 886
887 887 # All other frames should be related to a command that is currently
888 888 # receiving but is not active.
889 889 if frame.requestid in self._activecommands:
890 890 self._state = 'errored'
891 891 return self._makeerrorresult(
892 892 _('received frame for request that is still active: %d') %
893 893 frame.requestid)
894 894
895 895 if frame.requestid not in self._receivingcommands:
896 896 self._state = 'errored'
897 897 return self._makeerrorresult(
898 898 _('received frame for request that is not receiving: %d') %
899 899 frame.requestid)
900 900
901 901 entry = self._receivingcommands[frame.requestid]
902 902
903 903 if frame.typeid == FRAME_TYPE_COMMAND_REQUEST:
904 904 moreframes = frame.flags & FLAG_COMMAND_REQUEST_MORE_FRAMES
905 905 expectingdata = bool(frame.flags & FLAG_COMMAND_REQUEST_EXPECT_DATA)
906 906
907 907 if entry['requestdone']:
908 908 self._state = 'errored'
909 909 return self._makeerrorresult(
910 910 _('received command request frame when request frames '
911 911 'were supposedly done'))
912 912
913 913 if expectingdata != entry['expectingdata']:
914 914 self._state = 'errored'
915 915 return self._makeerrorresult(
916 916 _('mismatch between expect data flag and previous frame'))
917 917
918 918 entry['payload'].write(frame.payload)
919 919
920 920 if not moreframes:
921 921 entry['requestdone'] = True
922 922
923 923 if not moreframes and not expectingdata:
924 924 return self._makeruncommandresult(frame.requestid)
925 925
926 926 return self._makewantframeresult()
927 927
928 928 elif frame.typeid == FRAME_TYPE_COMMAND_DATA:
929 929 if not entry['expectingdata']:
930 930 self._state = 'errored'
931 931 return self._makeerrorresult(_(
932 932 'received command data frame for request that is not '
933 933 'expecting data: %d') % frame.requestid)
934 934
935 935 if entry['data'] is None:
936 936 entry['data'] = util.bytesio()
937 937
938 938 return self._handlecommanddataframe(frame, entry)
939 939 else:
940 940 self._state = 'errored'
941 941 return self._makeerrorresult(_(
942 942 'received unexpected frame type: %d') % frame.typeid)
943 943
944 944 def _handlecommanddataframe(self, frame, entry):
945 945 assert frame.typeid == FRAME_TYPE_COMMAND_DATA
946 946
947 947 # TODO support streaming data instead of buffering it.
948 948 entry['data'].write(frame.payload)
949 949
950 950 if frame.flags & FLAG_COMMAND_DATA_CONTINUATION:
951 951 return self._makewantframeresult()
952 952 elif frame.flags & FLAG_COMMAND_DATA_EOS:
953 953 entry['data'].seek(0)
954 954 return self._makeruncommandresult(frame.requestid)
955 955 else:
956 956 self._state = 'errored'
957 957 return self._makeerrorresult(_('command data frame without '
958 958 'flags'))
959 959
960 960 def _onframeerrored(self, frame):
961 961 return self._makeerrorresult(_('server already errored'))
962 962
963 963 class commandrequest(object):
964 964 """Represents a request to run a command."""
965 965
966 966 def __init__(self, requestid, name, args, datafh=None):
967 967 self.requestid = requestid
968 968 self.name = name
969 969 self.args = args
970 970 self.datafh = datafh
971 971 self.state = 'pending'
972 972
973 973 class clientreactor(object):
974 974 """Holds state of a client issuing frame-based protocol requests.
975 975
976 976 This is like ``serverreactor`` but for client-side state.
977 977
978 978 Each instance is bound to the lifetime of a connection. For persistent
979 979 connection transports using e.g. TCP sockets and speaking the raw
980 980 framing protocol, there will be a single instance for the lifetime of
981 981 the TCP socket. For transports where there are multiple discrete
982 982 interactions (say tunneled within in HTTP request), there will be a
983 983 separate instance for each distinct interaction.
984 984 """
985 985 def __init__(self, hasmultiplesend=False, buffersends=True):
986 986 """Create a new instance.
987 987
988 988 ``hasmultiplesend`` indicates whether multiple sends are supported
989 989 by the transport. When True, it is possible to send commands immediately
990 990 instead of buffering until the caller signals an intent to finish a
991 991 send operation.
992 992
993 993 ``buffercommands`` indicates whether sends should be buffered until the
994 994 last request has been issued.
995 995 """
996 996 self._hasmultiplesend = hasmultiplesend
997 997 self._buffersends = buffersends
998 998
999 999 self._canissuecommands = True
1000 1000 self._cansend = True
1001 1001
1002 1002 self._nextrequestid = 1
1003 1003 # We only support a single outgoing stream for now.
1004 1004 self._outgoingstream = stream(1)
1005 1005 self._pendingrequests = collections.deque()
1006 1006 self._activerequests = {}
1007 1007 self._incomingstreams = {}
1008 1008
1009 1009 def callcommand(self, name, args, datafh=None):
1010 1010 """Request that a command be executed.
1011 1011
1012 1012 Receives the command name, a dict of arguments to pass to the command,
1013 1013 and an optional file object containing the raw data for the command.
1014 1014
1015 1015 Returns a 3-tuple of (request, action, action data).
1016 1016 """
1017 1017 if not self._canissuecommands:
1018 1018 raise error.ProgrammingError('cannot issue new commands')
1019 1019
1020 1020 requestid = self._nextrequestid
1021 1021 self._nextrequestid += 2
1022 1022
1023 1023 request = commandrequest(requestid, name, args, datafh=datafh)
1024 1024
1025 1025 if self._buffersends:
1026 1026 self._pendingrequests.append(request)
1027 1027 return request, 'noop', {}
1028 1028 else:
1029 1029 if not self._cansend:
1030 1030 raise error.ProgrammingError('sends cannot be performed on '
1031 1031 'this instance')
1032 1032
1033 1033 if not self._hasmultiplesend:
1034 1034 self._cansend = False
1035 1035 self._canissuecommands = False
1036 1036
1037 1037 return request, 'sendframes', {
1038 1038 'framegen': self._makecommandframes(request),
1039 1039 }
1040 1040
1041 1041 def flushcommands(self):
1042 1042 """Request that all queued commands be sent.
1043 1043
1044 1044 If any commands are buffered, this will instruct the caller to send
1045 1045 them over the wire. If no commands are buffered it instructs the client
1046 1046 to no-op.
1047 1047
1048 1048 If instances aren't configured for multiple sends, no new command
1049 1049 requests are allowed after this is called.
1050 1050 """
1051 1051 if not self._pendingrequests:
1052 1052 return 'noop', {}
1053 1053
1054 1054 if not self._cansend:
1055 1055 raise error.ProgrammingError('sends cannot be performed on this '
1056 1056 'instance')
1057 1057
1058 1058 # If the instance only allows sending once, mark that we have fired
1059 1059 # our one shot.
1060 1060 if not self._hasmultiplesend:
1061 1061 self._canissuecommands = False
1062 1062 self._cansend = False
1063 1063
1064 1064 def makeframes():
1065 1065 while self._pendingrequests:
1066 1066 request = self._pendingrequests.popleft()
1067 1067 for frame in self._makecommandframes(request):
1068 1068 yield frame
1069 1069
1070 1070 return 'sendframes', {
1071 1071 'framegen': makeframes(),
1072 1072 }
1073 1073
1074 1074 def _makecommandframes(self, request):
1075 1075 """Emit frames to issue a command request.
1076 1076
1077 1077 As a side-effect, update request accounting to reflect its changed
1078 1078 state.
1079 1079 """
1080 1080 self._activerequests[request.requestid] = request
1081 1081 request.state = 'sending'
1082 1082
1083 1083 res = createcommandframes(self._outgoingstream,
1084 1084 request.requestid,
1085 1085 request.name,
1086 1086 request.args,
1087 1087 request.datafh)
1088 1088
1089 1089 for frame in res:
1090 1090 yield frame
1091 1091
1092 1092 request.state = 'sent'
1093 1093
1094 1094 def onframerecv(self, frame):
1095 1095 """Process a frame that has been received off the wire.
1096 1096
1097 1097 Returns a 2-tuple of (action, meta) describing further action the
1098 1098 caller needs to take as a result of receiving this frame.
1099 1099 """
1100 1100 if frame.streamid % 2:
1101 1101 return 'error', {
1102 1102 'message': (
1103 1103 _('received frame with odd numbered stream ID: %d') %
1104 1104 frame.streamid),
1105 1105 }
1106 1106
1107 1107 if frame.streamid not in self._incomingstreams:
1108 1108 if not frame.streamflags & STREAM_FLAG_BEGIN_STREAM:
1109 1109 return 'error', {
1110 1110 'message': _('received frame on unknown stream '
1111 1111 'without beginning of stream flag set'),
1112 1112 }
1113 1113
1114 1114 self._incomingstreams[frame.streamid] = stream(frame.streamid)
1115 1115
1116 1116 if frame.streamflags & STREAM_FLAG_ENCODING_APPLIED:
1117 1117 raise error.ProgrammingError('support for decoding stream '
1118 1118 'payloads not yet implemneted')
1119 1119
1120 1120 if frame.streamflags & STREAM_FLAG_END_STREAM:
1121 1121 del self._incomingstreams[frame.streamid]
1122 1122
1123 1123 if frame.requestid not in self._activerequests:
1124 1124 return 'error', {
1125 1125 'message': (_('received frame for inactive request ID: %d') %
1126 1126 frame.requestid),
1127 1127 }
1128 1128
1129 1129 request = self._activerequests[frame.requestid]
1130 1130 request.state = 'receiving'
1131 1131
1132 1132 handlers = {
1133 1133 FRAME_TYPE_COMMAND_RESPONSE: self._oncommandresponseframe,
1134 1134 FRAME_TYPE_ERROR_RESPONSE: self._onerrorresponseframe,
1135 1135 }
1136 1136
1137 1137 meth = handlers.get(frame.typeid)
1138 1138 if not meth:
1139 1139 raise error.ProgrammingError('unhandled frame type: %d' %
1140 1140 frame.typeid)
1141 1141
1142 1142 return meth(request, frame)
1143 1143
1144 1144 def _oncommandresponseframe(self, request, frame):
1145 1145 if frame.flags & FLAG_COMMAND_RESPONSE_EOS:
1146 1146 request.state = 'received'
1147 1147 del self._activerequests[request.requestid]
1148 1148
1149 1149 return 'responsedata', {
1150 1150 'request': request,
1151 1151 'expectmore': frame.flags & FLAG_COMMAND_RESPONSE_CONTINUATION,
1152 1152 'eos': frame.flags & FLAG_COMMAND_RESPONSE_EOS,
1153 1153 'data': frame.payload,
1154 1154 }
1155 1155
1156 1156 def _onerrorresponseframe(self, request, frame):
1157 1157 request.state = 'errored'
1158 1158 del self._activerequests[request.requestid]
1159 1159
1160 1160 # The payload should be a CBOR map.
1161 m = cbor.loads(frame.payload)
1161 m = cborutil.decodeall(frame.payload)[0]
1162 1162
1163 1163 return 'error', {
1164 1164 'request': request,
1165 1165 'type': m['type'],
1166 1166 'message': m['message'],
1167 1167 }
@@ -1,571 +1,571 b''
1 1 #require no-chg
2 2
3 3 $ . $TESTDIR/wireprotohelpers.sh
4 4 $ enabledummycommands
5 5
6 6 $ hg init server
7 7 $ cat > server/.hg/hgrc << EOF
8 8 > [experimental]
9 9 > web.apiserver = true
10 10 > EOF
11 11 $ hg -R server serve -p $HGPORT -d --pid-file hg.pid
12 12 $ cat hg.pid > $DAEMON_PIDS
13 13
14 14 HTTP v2 protocol not enabled by default
15 15
16 16 $ sendhttpraw << EOF
17 17 > httprequest GET api/$HTTPV2
18 18 > user-agent: test
19 19 > EOF
20 20 using raw connection to peer
21 21 s> GET /api/exp-http-v2-0001 HTTP/1.1\r\n
22 22 s> Accept-Encoding: identity\r\n
23 23 s> user-agent: test\r\n
24 24 s> host: $LOCALIP:$HGPORT\r\n (glob)
25 25 s> \r\n
26 26 s> makefile('rb', None)
27 27 s> HTTP/1.1 404 Not Found\r\n
28 28 s> Server: testing stub value\r\n
29 29 s> Date: $HTTP_DATE$\r\n
30 30 s> Content-Type: text/plain\r\n
31 31 s> Content-Length: 33\r\n
32 32 s> \r\n
33 33 s> API exp-http-v2-0001 not enabled\n
34 34
35 35 Restart server with support for HTTP v2 API
36 36
37 37 $ killdaemons.py
38 38 $ enablehttpv2 server
39 39 $ hg -R server serve -p $HGPORT -d --pid-file hg.pid
40 40 $ cat hg.pid > $DAEMON_PIDS
41 41
42 42 Request to unknown command yields 404
43 43
44 44 $ sendhttpraw << EOF
45 45 > httprequest POST api/$HTTPV2/ro/badcommand
46 46 > user-agent: test
47 47 > EOF
48 48 using raw connection to peer
49 49 s> POST /api/exp-http-v2-0001/ro/badcommand HTTP/1.1\r\n
50 50 s> Accept-Encoding: identity\r\n
51 51 s> user-agent: test\r\n
52 52 s> host: $LOCALIP:$HGPORT\r\n (glob)
53 53 s> \r\n
54 54 s> makefile('rb', None)
55 55 s> HTTP/1.1 404 Not Found\r\n
56 56 s> Server: testing stub value\r\n
57 57 s> Date: $HTTP_DATE$\r\n
58 58 s> Content-Type: text/plain\r\n
59 59 s> Content-Length: 42\r\n
60 60 s> \r\n
61 61 s> unknown wire protocol command: badcommand\n
62 62
63 63 GET to read-only command yields a 405
64 64
65 65 $ sendhttpraw << EOF
66 66 > httprequest GET api/$HTTPV2/ro/customreadonly
67 67 > user-agent: test
68 68 > EOF
69 69 using raw connection to peer
70 70 s> GET /api/exp-http-v2-0001/ro/customreadonly HTTP/1.1\r\n
71 71 s> Accept-Encoding: identity\r\n
72 72 s> user-agent: test\r\n
73 73 s> host: $LOCALIP:$HGPORT\r\n (glob)
74 74 s> \r\n
75 75 s> makefile('rb', None)
76 76 s> HTTP/1.1 405 Method Not Allowed\r\n
77 77 s> Server: testing stub value\r\n
78 78 s> Date: $HTTP_DATE$\r\n
79 79 s> Allow: POST\r\n
80 80 s> Content-Length: 30\r\n
81 81 s> \r\n
82 82 s> commands require POST requests
83 83
84 84 Missing Accept header results in 406
85 85
86 86 $ sendhttpraw << EOF
87 87 > httprequest POST api/$HTTPV2/ro/customreadonly
88 88 > user-agent: test
89 89 > EOF
90 90 using raw connection to peer
91 91 s> POST /api/exp-http-v2-0001/ro/customreadonly HTTP/1.1\r\n
92 92 s> Accept-Encoding: identity\r\n
93 93 s> user-agent: test\r\n
94 94 s> host: $LOCALIP:$HGPORT\r\n (glob)
95 95 s> \r\n
96 96 s> makefile('rb', None)
97 97 s> HTTP/1.1 406 Not Acceptable\r\n
98 98 s> Server: testing stub value\r\n
99 99 s> Date: $HTTP_DATE$\r\n
100 100 s> Content-Type: text/plain\r\n
101 101 s> Content-Length: 85\r\n
102 102 s> \r\n
103 103 s> client MUST specify Accept header with value: application/mercurial-exp-framing-0005\n
104 104
105 105 Bad Accept header results in 406
106 106
107 107 $ sendhttpraw << EOF
108 108 > httprequest POST api/$HTTPV2/ro/customreadonly
109 109 > accept: invalid
110 110 > user-agent: test
111 111 > EOF
112 112 using raw connection to peer
113 113 s> POST /api/exp-http-v2-0001/ro/customreadonly HTTP/1.1\r\n
114 114 s> Accept-Encoding: identity\r\n
115 115 s> accept: invalid\r\n
116 116 s> user-agent: test\r\n
117 117 s> host: $LOCALIP:$HGPORT\r\n (glob)
118 118 s> \r\n
119 119 s> makefile('rb', None)
120 120 s> HTTP/1.1 406 Not Acceptable\r\n
121 121 s> Server: testing stub value\r\n
122 122 s> Date: $HTTP_DATE$\r\n
123 123 s> Content-Type: text/plain\r\n
124 124 s> Content-Length: 85\r\n
125 125 s> \r\n
126 126 s> client MUST specify Accept header with value: application/mercurial-exp-framing-0005\n
127 127
128 128 Bad Content-Type header results in 415
129 129
130 130 $ sendhttpraw << EOF
131 131 > httprequest POST api/$HTTPV2/ro/customreadonly
132 132 > accept: $MEDIATYPE
133 133 > user-agent: test
134 134 > content-type: badmedia
135 135 > EOF
136 136 using raw connection to peer
137 137 s> POST /api/exp-http-v2-0001/ro/customreadonly HTTP/1.1\r\n
138 138 s> Accept-Encoding: identity\r\n
139 139 s> accept: application/mercurial-exp-framing-0005\r\n
140 140 s> content-type: badmedia\r\n
141 141 s> user-agent: test\r\n
142 142 s> host: $LOCALIP:$HGPORT\r\n (glob)
143 143 s> \r\n
144 144 s> makefile('rb', None)
145 145 s> HTTP/1.1 415 Unsupported Media Type\r\n
146 146 s> Server: testing stub value\r\n
147 147 s> Date: $HTTP_DATE$\r\n
148 148 s> Content-Type: text/plain\r\n
149 149 s> Content-Length: 88\r\n
150 150 s> \r\n
151 151 s> client MUST send Content-Type header with value: application/mercurial-exp-framing-0005\n
152 152
153 153 Request to read-only command works out of the box
154 154
155 155 $ sendhttpraw << EOF
156 156 > httprequest POST api/$HTTPV2/ro/customreadonly
157 157 > accept: $MEDIATYPE
158 158 > content-type: $MEDIATYPE
159 159 > user-agent: test
160 160 > frame 1 1 stream-begin command-request new cbor:{b'name': b'customreadonly'}
161 161 > EOF
162 162 using raw connection to peer
163 163 s> POST /api/exp-http-v2-0001/ro/customreadonly HTTP/1.1\r\n
164 164 s> Accept-Encoding: identity\r\n
165 165 s> *\r\n (glob)
166 166 s> content-type: application/mercurial-exp-framing-0005\r\n
167 167 s> user-agent: test\r\n
168 168 s> content-length: 29\r\n
169 169 s> host: $LOCALIP:$HGPORT\r\n (glob)
170 170 s> \r\n
171 171 s> \x15\x00\x00\x01\x00\x01\x01\x11\xa1DnameNcustomreadonly
172 172 s> makefile('rb', None)
173 173 s> HTTP/1.1 200 OK\r\n
174 174 s> Server: testing stub value\r\n
175 175 s> Date: $HTTP_DATE$\r\n
176 176 s> Content-Type: application/mercurial-exp-framing-0005\r\n
177 177 s> Transfer-Encoding: chunked\r\n
178 178 s> \r\n
179 179 s> 32\r\n
180 180 s> *\x00\x00\x01\x00\x02\x012\xa1FstatusBokX\x1dcustomreadonly bytes response
181 181 s> \r\n
182 182 s> 0\r\n
183 183 s> \r\n
184 184
185 185 $ sendhttpv2peer << EOF
186 186 > command customreadonly
187 187 > EOF
188 188 creating http peer for wire protocol version 2
189 189 sending customreadonly command
190 190 s> POST /api/exp-http-v2-0001/ro/customreadonly HTTP/1.1\r\n
191 191 s> Accept-Encoding: identity\r\n
192 192 s> accept: application/mercurial-exp-framing-0005\r\n
193 193 s> content-type: application/mercurial-exp-framing-0005\r\n
194 194 s> content-length: 29\r\n
195 195 s> host: $LOCALIP:$HGPORT\r\n (glob)
196 196 s> user-agent: Mercurial debugwireproto\r\n
197 197 s> \r\n
198 198 s> \x15\x00\x00\x01\x00\x01\x01\x11\xa1DnameNcustomreadonly
199 199 s> makefile('rb', None)
200 200 s> HTTP/1.1 200 OK\r\n
201 201 s> Server: testing stub value\r\n
202 202 s> Date: $HTTP_DATE$\r\n
203 203 s> Content-Type: application/mercurial-exp-framing-0005\r\n
204 204 s> Transfer-Encoding: chunked\r\n
205 205 s> \r\n
206 206 s> 32\r\n
207 207 s> *\x00\x00\x01\x00\x02\x012
208 208 s> \xa1FstatusBokX\x1dcustomreadonly bytes response
209 209 s> \r\n
210 210 received frame(size=42; request=1; stream=2; streamflags=stream-begin; type=command-response; flags=eos)
211 211 s> 0\r\n
212 212 s> \r\n
213 213 response: [
214 214 {
215 215 b'status': b'ok'
216 216 },
217 217 b'customreadonly bytes response'
218 218 ]
219 219
220 220 Request to read-write command fails because server is read-only by default
221 221
222 222 GET to read-write request yields 405
223 223
224 224 $ sendhttpraw << EOF
225 225 > httprequest GET api/$HTTPV2/rw/customreadonly
226 226 > user-agent: test
227 227 > EOF
228 228 using raw connection to peer
229 229 s> GET /api/exp-http-v2-0001/rw/customreadonly HTTP/1.1\r\n
230 230 s> Accept-Encoding: identity\r\n
231 231 s> user-agent: test\r\n
232 232 s> host: $LOCALIP:$HGPORT\r\n (glob)
233 233 s> \r\n
234 234 s> makefile('rb', None)
235 235 s> HTTP/1.1 405 Method Not Allowed\r\n
236 236 s> Server: testing stub value\r\n
237 237 s> Date: $HTTP_DATE$\r\n
238 238 s> Allow: POST\r\n
239 239 s> Content-Length: 30\r\n
240 240 s> \r\n
241 241 s> commands require POST requests
242 242
243 243 Even for unknown commands
244 244
245 245 $ sendhttpraw << EOF
246 246 > httprequest GET api/$HTTPV2/rw/badcommand
247 247 > user-agent: test
248 248 > EOF
249 249 using raw connection to peer
250 250 s> GET /api/exp-http-v2-0001/rw/badcommand HTTP/1.1\r\n
251 251 s> Accept-Encoding: identity\r\n
252 252 s> user-agent: test\r\n
253 253 s> host: $LOCALIP:$HGPORT\r\n (glob)
254 254 s> \r\n
255 255 s> makefile('rb', None)
256 256 s> HTTP/1.1 405 Method Not Allowed\r\n
257 257 s> Server: testing stub value\r\n
258 258 s> Date: $HTTP_DATE$\r\n
259 259 s> Allow: POST\r\n
260 260 s> Content-Length: 30\r\n
261 261 s> \r\n
262 262 s> commands require POST requests
263 263
264 264 SSL required by default
265 265
266 266 $ sendhttpraw << EOF
267 267 > httprequest POST api/$HTTPV2/rw/customreadonly
268 268 > user-agent: test
269 269 > EOF
270 270 using raw connection to peer
271 271 s> POST /api/exp-http-v2-0001/rw/customreadonly HTTP/1.1\r\n
272 272 s> Accept-Encoding: identity\r\n
273 273 s> user-agent: test\r\n
274 274 s> host: $LOCALIP:$HGPORT\r\n (glob)
275 275 s> \r\n
276 276 s> makefile('rb', None)
277 277 s> HTTP/1.1 403 ssl required\r\n
278 278 s> Server: testing stub value\r\n
279 279 s> Date: $HTTP_DATE$\r\n
280 280 s> Content-Length: 17\r\n
281 281 s> \r\n
282 282 s> permission denied
283 283
284 284 Restart server to allow non-ssl read-write operations
285 285
286 286 $ killdaemons.py
287 287 $ cat > server/.hg/hgrc << EOF
288 288 > [experimental]
289 289 > web.apiserver = true
290 290 > web.api.http-v2 = true
291 291 > [web]
292 292 > push_ssl = false
293 293 > allow-push = *
294 294 > EOF
295 295
296 296 $ hg -R server serve -p $HGPORT -d --pid-file hg.pid -E error.log
297 297 $ cat hg.pid > $DAEMON_PIDS
298 298
299 299 Authorized request for valid read-write command works
300 300
301 301 $ sendhttpraw << EOF
302 302 > httprequest POST api/$HTTPV2/rw/customreadonly
303 303 > user-agent: test
304 304 > accept: $MEDIATYPE
305 305 > content-type: $MEDIATYPE
306 306 > frame 1 1 stream-begin command-request new cbor:{b'name': b'customreadonly'}
307 307 > EOF
308 308 using raw connection to peer
309 309 s> POST /api/exp-http-v2-0001/rw/customreadonly HTTP/1.1\r\n
310 310 s> Accept-Encoding: identity\r\n
311 311 s> accept: application/mercurial-exp-framing-0005\r\n
312 312 s> content-type: application/mercurial-exp-framing-0005\r\n
313 313 s> user-agent: test\r\n
314 314 s> content-length: 29\r\n
315 315 s> host: $LOCALIP:$HGPORT\r\n (glob)
316 316 s> \r\n
317 317 s> \x15\x00\x00\x01\x00\x01\x01\x11\xa1DnameNcustomreadonly
318 318 s> makefile('rb', None)
319 319 s> HTTP/1.1 200 OK\r\n
320 320 s> Server: testing stub value\r\n
321 321 s> Date: $HTTP_DATE$\r\n
322 322 s> Content-Type: application/mercurial-exp-framing-0005\r\n
323 323 s> Transfer-Encoding: chunked\r\n
324 324 s> \r\n
325 325 s> 32\r\n
326 326 s> *\x00\x00\x01\x00\x02\x012\xa1FstatusBokX\x1dcustomreadonly bytes response
327 327 s> \r\n
328 328 s> 0\r\n
329 329 s> \r\n
330 330
331 331 Authorized request for unknown command is rejected
332 332
333 333 $ sendhttpraw << EOF
334 334 > httprequest POST api/$HTTPV2/rw/badcommand
335 335 > user-agent: test
336 336 > accept: $MEDIATYPE
337 337 > EOF
338 338 using raw connection to peer
339 339 s> POST /api/exp-http-v2-0001/rw/badcommand HTTP/1.1\r\n
340 340 s> Accept-Encoding: identity\r\n
341 341 s> accept: application/mercurial-exp-framing-0005\r\n
342 342 s> user-agent: test\r\n
343 343 s> host: $LOCALIP:$HGPORT\r\n (glob)
344 344 s> \r\n
345 345 s> makefile('rb', None)
346 346 s> HTTP/1.1 404 Not Found\r\n
347 347 s> Server: testing stub value\r\n
348 348 s> Date: $HTTP_DATE$\r\n
349 349 s> Content-Type: text/plain\r\n
350 350 s> Content-Length: 42\r\n
351 351 s> \r\n
352 352 s> unknown wire protocol command: badcommand\n
353 353
354 354 debugreflect isn't enabled by default
355 355
356 356 $ sendhttpraw << EOF
357 357 > httprequest POST api/$HTTPV2/ro/debugreflect
358 358 > user-agent: test
359 359 > EOF
360 360 using raw connection to peer
361 361 s> POST /api/exp-http-v2-0001/ro/debugreflect HTTP/1.1\r\n
362 362 s> Accept-Encoding: identity\r\n
363 363 s> user-agent: test\r\n
364 364 s> host: $LOCALIP:$HGPORT\r\n (glob)
365 365 s> \r\n
366 366 s> makefile('rb', None)
367 367 s> HTTP/1.1 404 Not Found\r\n
368 368 s> Server: testing stub value\r\n
369 369 s> Date: $HTTP_DATE$\r\n
370 370 s> Content-Type: text/plain\r\n
371 371 s> Content-Length: 34\r\n
372 372 s> \r\n
373 373 s> debugreflect service not available
374 374
375 375 Restart server to get debugreflect endpoint
376 376
377 377 $ killdaemons.py
378 378 $ cat > server/.hg/hgrc << EOF
379 379 > [experimental]
380 380 > web.apiserver = true
381 381 > web.api.debugreflect = true
382 382 > web.api.http-v2 = true
383 383 > [web]
384 384 > push_ssl = false
385 385 > allow-push = *
386 386 > EOF
387 387
388 388 $ hg -R server serve -p $HGPORT -d --pid-file hg.pid -E error.log
389 389 $ cat hg.pid > $DAEMON_PIDS
390 390
391 391 Command frames can be reflected via debugreflect
392 392
393 393 $ sendhttpraw << EOF
394 394 > httprequest POST api/$HTTPV2/ro/debugreflect
395 395 > accept: $MEDIATYPE
396 396 > content-type: $MEDIATYPE
397 397 > user-agent: test
398 398 > frame 1 1 stream-begin command-request new cbor:{b'name': b'command1', b'args': {b'foo': b'val1', b'bar1': b'val'}}
399 399 > EOF
400 400 using raw connection to peer
401 401 s> POST /api/exp-http-v2-0001/ro/debugreflect HTTP/1.1\r\n
402 402 s> Accept-Encoding: identity\r\n
403 403 s> accept: application/mercurial-exp-framing-0005\r\n
404 404 s> content-type: application/mercurial-exp-framing-0005\r\n
405 405 s> user-agent: test\r\n
406 406 s> content-length: 47\r\n
407 407 s> host: $LOCALIP:$HGPORT\r\n (glob)
408 408 s> \r\n
409 s> \'\x00\x00\x01\x00\x01\x01\x11\xa2Dargs\xa2CfooDval1Dbar1CvalDnameHcommand1
409 s> \'\x00\x00\x01\x00\x01\x01\x11\xa2Dargs\xa2Dbar1CvalCfooDval1DnameHcommand1
410 410 s> makefile('rb', None)
411 411 s> HTTP/1.1 200 OK\r\n
412 412 s> Server: testing stub value\r\n
413 413 s> Date: $HTTP_DATE$\r\n
414 414 s> Content-Type: text/plain\r\n
415 415 s> Content-Length: 205\r\n
416 416 s> \r\n
417 s> received: 1 1 1 \xa2Dargs\xa2CfooDval1Dbar1CvalDnameHcommand1\n
417 s> received: 1 1 1 \xa2Dargs\xa2Dbar1CvalCfooDval1DnameHcommand1\n
418 418 s> ["runcommand", {"args": {"bar1": "val", "foo": "val1"}, "command": "command1", "data": null, "requestid": 1}]\n
419 419 s> received: <no frame>\n
420 420 s> {"action": "noop"}
421 421
422 422 Multiple requests to regular command URL are not allowed
423 423
424 424 $ sendhttpraw << EOF
425 425 > httprequest POST api/$HTTPV2/ro/customreadonly
426 426 > accept: $MEDIATYPE
427 427 > content-type: $MEDIATYPE
428 428 > user-agent: test
429 429 > frame 1 1 stream-begin command-request new cbor:{b'name': b'customreadonly'}
430 430 > EOF
431 431 using raw connection to peer
432 432 s> POST /api/exp-http-v2-0001/ro/customreadonly HTTP/1.1\r\n
433 433 s> Accept-Encoding: identity\r\n
434 434 s> accept: application/mercurial-exp-framing-0005\r\n
435 435 s> content-type: application/mercurial-exp-framing-0005\r\n
436 436 s> user-agent: test\r\n
437 437 s> content-length: 29\r\n
438 438 s> host: $LOCALIP:$HGPORT\r\n (glob)
439 439 s> \r\n
440 440 s> \x15\x00\x00\x01\x00\x01\x01\x11\xa1DnameNcustomreadonly
441 441 s> makefile('rb', None)
442 442 s> HTTP/1.1 200 OK\r\n
443 443 s> Server: testing stub value\r\n
444 444 s> Date: $HTTP_DATE$\r\n
445 445 s> Content-Type: application/mercurial-exp-framing-0005\r\n
446 446 s> Transfer-Encoding: chunked\r\n
447 447 s> \r\n
448 448 s> 32\r\n
449 449 s> *\x00\x00\x01\x00\x02\x012\xa1FstatusBokX\x1dcustomreadonly bytes response
450 450 s> \r\n
451 451 s> 0\r\n
452 452 s> \r\n
453 453
454 454 Multiple requests to "multirequest" URL are allowed
455 455
456 456 $ sendhttpraw << EOF
457 457 > httprequest POST api/$HTTPV2/ro/multirequest
458 458 > accept: $MEDIATYPE
459 459 > content-type: $MEDIATYPE
460 460 > user-agent: test
461 461 > frame 1 1 stream-begin command-request new cbor:{b'name': b'customreadonly'}
462 462 > frame 3 1 0 command-request new cbor:{b'name': b'customreadonly'}
463 463 > EOF
464 464 using raw connection to peer
465 465 s> POST /api/exp-http-v2-0001/ro/multirequest HTTP/1.1\r\n
466 466 s> Accept-Encoding: identity\r\n
467 467 s> *\r\n (glob)
468 468 s> *\r\n (glob)
469 469 s> user-agent: test\r\n
470 470 s> content-length: 58\r\n
471 471 s> host: $LOCALIP:$HGPORT\r\n (glob)
472 472 s> \r\n
473 473 s> \x15\x00\x00\x01\x00\x01\x01\x11\xa1DnameNcustomreadonly\x15\x00\x00\x03\x00\x01\x00\x11\xa1DnameNcustomreadonly
474 474 s> makefile('rb', None)
475 475 s> HTTP/1.1 200 OK\r\n
476 476 s> Server: testing stub value\r\n
477 477 s> Date: $HTTP_DATE$\r\n
478 478 s> Content-Type: application/mercurial-exp-framing-0005\r\n
479 479 s> Transfer-Encoding: chunked\r\n
480 480 s> \r\n
481 481 s> 32\r\n
482 482 s> *\x00\x00\x01\x00\x02\x012\xa1FstatusBokX\x1dcustomreadonly bytes response
483 483 s> \r\n
484 484 s> 32\r\n
485 485 s> *\x00\x00\x03\x00\x02\x002\xa1FstatusBokX\x1dcustomreadonly bytes response
486 486 s> \r\n
487 487 s> 0\r\n
488 488 s> \r\n
489 489
490 490 Interleaved requests to "multirequest" are processed
491 491
492 492 $ sendhttpraw << EOF
493 493 > httprequest POST api/$HTTPV2/ro/multirequest
494 494 > accept: $MEDIATYPE
495 495 > content-type: $MEDIATYPE
496 496 > user-agent: test
497 497 > frame 1 1 stream-begin command-request new|more \xa2Dargs\xa1Inamespace
498 498 > frame 3 1 0 command-request new|more \xa2Dargs\xa1Inamespace
499 499 > frame 3 1 0 command-request continuation JnamespacesDnameHlistkeys
500 500 > frame 1 1 0 command-request continuation IbookmarksDnameHlistkeys
501 501 > EOF
502 502 using raw connection to peer
503 503 s> POST /api/exp-http-v2-0001/ro/multirequest HTTP/1.1\r\n
504 504 s> Accept-Encoding: identity\r\n
505 505 s> accept: application/mercurial-exp-framing-0005\r\n
506 506 s> content-type: application/mercurial-exp-framing-0005\r\n
507 507 s> user-agent: test\r\n
508 508 s> content-length: 115\r\n
509 509 s> host: $LOCALIP:$HGPORT\r\n (glob)
510 510 s> \r\n
511 511 s> \x11\x00\x00\x01\x00\x01\x01\x15\xa2Dargs\xa1Inamespace\x11\x00\x00\x03\x00\x01\x00\x15\xa2Dargs\xa1Inamespace\x19\x00\x00\x03\x00\x01\x00\x12JnamespacesDnameHlistkeys\x18\x00\x00\x01\x00\x01\x00\x12IbookmarksDnameHlistkeys
512 512 s> makefile('rb', None)
513 513 s> HTTP/1.1 200 OK\r\n
514 514 s> Server: testing stub value\r\n
515 515 s> Date: $HTTP_DATE$\r\n
516 516 s> Content-Type: application/mercurial-exp-framing-0005\r\n
517 517 s> Transfer-Encoding: chunked\r\n
518 518 s> \r\n
519 519 s> 33\r\n
520 520 s> +\x00\x00\x03\x00\x02\x012\xa1FstatusBok\xa3Fphases@Ibookmarks@Jnamespaces@
521 521 s> \r\n
522 522 s> 14\r\n
523 523 s> \x0c\x00\x00\x01\x00\x02\x002\xa1FstatusBok\xa0
524 524 s> \r\n
525 525 s> 0\r\n
526 526 s> \r\n
527 527
528 528 Restart server to disable read-write access
529 529
530 530 $ killdaemons.py
531 531 $ cat > server/.hg/hgrc << EOF
532 532 > [experimental]
533 533 > web.apiserver = true
534 534 > web.api.debugreflect = true
535 535 > web.api.http-v2 = true
536 536 > [web]
537 537 > push_ssl = false
538 538 > EOF
539 539
540 540 $ hg -R server serve -p $HGPORT -d --pid-file hg.pid -E error.log
541 541 $ cat hg.pid > $DAEMON_PIDS
542 542
543 543 Attempting to run a read-write command via multirequest on read-only URL is not allowed
544 544
545 545 $ sendhttpraw << EOF
546 546 > httprequest POST api/$HTTPV2/ro/multirequest
547 547 > accept: $MEDIATYPE
548 548 > content-type: $MEDIATYPE
549 549 > user-agent: test
550 550 > frame 1 1 stream-begin command-request new cbor:{b'name': b'pushkey'}
551 551 > EOF
552 552 using raw connection to peer
553 553 s> POST /api/exp-http-v2-0001/ro/multirequest HTTP/1.1\r\n
554 554 s> Accept-Encoding: identity\r\n
555 555 s> accept: application/mercurial-exp-framing-0005\r\n
556 556 s> content-type: application/mercurial-exp-framing-0005\r\n
557 557 s> user-agent: test\r\n
558 558 s> content-length: 22\r\n
559 559 s> host: $LOCALIP:$HGPORT\r\n (glob)
560 560 s> \r\n
561 561 s> \x0e\x00\x00\x01\x00\x01\x01\x11\xa1DnameGpushkey
562 562 s> makefile('rb', None)
563 563 s> HTTP/1.1 403 Forbidden\r\n
564 564 s> Server: testing stub value\r\n
565 565 s> Date: $HTTP_DATE$\r\n
566 566 s> Content-Type: text/plain\r\n
567 567 s> Content-Length: 52\r\n
568 568 s> \r\n
569 569 s> insufficient permissions to execute command: pushkey
570 570
571 571 $ cat error.log
@@ -1,91 +1,91 b''
1 1 $ . $TESTDIR/wireprotohelpers.sh
2 2
3 3 $ hg init server
4 4 $ enablehttpv2 server
5 5 $ cd server
6 6 $ cat >> .hg/hgrc << EOF
7 7 > [web]
8 8 > push_ssl = false
9 9 > allow-push = *
10 10 > EOF
11 11 $ hg debugdrawdag << EOF
12 12 > C D
13 13 > |/
14 14 > B
15 15 > |
16 16 > A
17 17 > EOF
18 18
19 19 $ hg serve -p $HGPORT -d --pid-file hg.pid -E error.log
20 20 $ cat hg.pid > $DAEMON_PIDS
21 21
22 22 pushkey for a bookmark works
23 23
24 24 $ sendhttpv2peer << EOF
25 25 > command pushkey
26 26 > namespace bookmarks
27 27 > key @
28 28 > old
29 29 > new 426bada5c67598ca65036d57d9e4b64b0c1ce7a0
30 30 > EOF
31 31 creating http peer for wire protocol version 2
32 32 sending pushkey command
33 33 s> *\r\n (glob)
34 34 s> Accept-Encoding: identity\r\n
35 35 s> accept: application/mercurial-exp-framing-0005\r\n
36 36 s> content-type: application/mercurial-exp-framing-0005\r\n
37 37 s> content-length: 105\r\n
38 38 s> host: $LOCALIP:$HGPORT\r\n (glob)
39 39 s> user-agent: Mercurial debugwireproto\r\n
40 40 s> \r\n
41 s> a\x00\x00\x01\x00\x01\x01\x11\xa2Dargs\xa4CkeyA@CnewX(426bada5c67598ca65036d57d9e4b64b0c1ce7a0Cold@InamespaceIbookmarksDnameGpushkey
41 s> a\x00\x00\x01\x00\x01\x01\x11\xa2Dargs\xa4CkeyA@InamespaceIbookmarksCnewX(426bada5c67598ca65036d57d9e4b64b0c1ce7a0Cold@DnameGpushkey
42 42 s> makefile('rb', None)
43 43 s> HTTP/1.1 200 OK\r\n
44 44 s> Server: testing stub value\r\n
45 45 s> Date: $HTTP_DATE$\r\n
46 46 s> Content-Type: application/mercurial-exp-framing-0005\r\n
47 47 s> Transfer-Encoding: chunked\r\n
48 48 s> \r\n
49 49 s> 14\r\n
50 50 s> \x0c\x00\x00\x01\x00\x02\x012
51 51 s> \xa1FstatusBok\xf5
52 52 s> \r\n
53 53 received frame(size=12; request=1; stream=2; streamflags=stream-begin; type=command-response; flags=eos)
54 54 s> 0\r\n
55 55 s> \r\n
56 56 response: True
57 57
58 58 $ sendhttpv2peer << EOF
59 59 > command listkeys
60 60 > namespace bookmarks
61 61 > EOF
62 62 creating http peer for wire protocol version 2
63 63 sending listkeys command
64 64 s> POST /api/exp-http-v2-0001/ro/listkeys HTTP/1.1\r\n
65 65 s> Accept-Encoding: identity\r\n
66 66 s> accept: application/mercurial-exp-framing-0005\r\n
67 67 s> content-type: application/mercurial-exp-framing-0005\r\n
68 68 s> content-length: 49\r\n
69 69 s> host: $LOCALIP:$HGPORT\r\n (glob)
70 70 s> user-agent: Mercurial debugwireproto\r\n
71 71 s> \r\n
72 72 s> )\x00\x00\x01\x00\x01\x01\x11\xa2Dargs\xa1InamespaceIbookmarksDnameHlistkeys
73 73 s> makefile('rb', None)
74 74 s> HTTP/1.1 200 OK\r\n
75 75 s> Server: testing stub value\r\n
76 76 s> Date: $HTTP_DATE$\r\n
77 77 s> Content-Type: application/mercurial-exp-framing-0005\r\n
78 78 s> Transfer-Encoding: chunked\r\n
79 79 s> \r\n
80 80 s> 40\r\n
81 81 s> 8\x00\x00\x01\x00\x02\x012
82 82 s> \xa1FstatusBok\xa1A@X(426bada5c67598ca65036d57d9e4b64b0c1ce7a0
83 83 s> \r\n
84 84 received frame(size=56; request=1; stream=2; streamflags=stream-begin; type=command-response; flags=eos)
85 85 s> 0\r\n
86 86 s> \r\n
87 87 response: {
88 88 b'@': b'426bada5c67598ca65036d57d9e4b64b0c1ce7a0'
89 89 }
90 90
91 91 $ cat error.log
General Comments 0
You need to be logged in to leave comments. Login now