##// END OF EJS Templates
wireprotov2: server support for sending content redirects...
Gregory Szorc -
r40061:b099e603 default
parent child Browse files
Show More
@@ -1,1346 +1,1388 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 20 )
21 21 from . import (
22 22 encoding,
23 23 error,
24 pycompat,
24 25 util,
25 26 wireprototypes,
26 27 )
27 28 from .utils import (
28 29 cborutil,
29 30 stringutil,
30 31 )
31 32
32 33 FRAME_HEADER_SIZE = 8
33 34 DEFAULT_MAX_FRAME_SIZE = 32768
34 35
35 36 STREAM_FLAG_BEGIN_STREAM = 0x01
36 37 STREAM_FLAG_END_STREAM = 0x02
37 38 STREAM_FLAG_ENCODING_APPLIED = 0x04
38 39
39 40 STREAM_FLAGS = {
40 41 b'stream-begin': STREAM_FLAG_BEGIN_STREAM,
41 42 b'stream-end': STREAM_FLAG_END_STREAM,
42 43 b'encoded': STREAM_FLAG_ENCODING_APPLIED,
43 44 }
44 45
45 46 FRAME_TYPE_COMMAND_REQUEST = 0x01
46 47 FRAME_TYPE_COMMAND_DATA = 0x02
47 48 FRAME_TYPE_COMMAND_RESPONSE = 0x03
48 49 FRAME_TYPE_ERROR_RESPONSE = 0x05
49 50 FRAME_TYPE_TEXT_OUTPUT = 0x06
50 51 FRAME_TYPE_PROGRESS = 0x07
51 52 FRAME_TYPE_STREAM_SETTINGS = 0x08
52 53
53 54 FRAME_TYPES = {
54 55 b'command-request': FRAME_TYPE_COMMAND_REQUEST,
55 56 b'command-data': FRAME_TYPE_COMMAND_DATA,
56 57 b'command-response': FRAME_TYPE_COMMAND_RESPONSE,
57 58 b'error-response': FRAME_TYPE_ERROR_RESPONSE,
58 59 b'text-output': FRAME_TYPE_TEXT_OUTPUT,
59 60 b'progress': FRAME_TYPE_PROGRESS,
60 61 b'stream-settings': FRAME_TYPE_STREAM_SETTINGS,
61 62 }
62 63
63 64 FLAG_COMMAND_REQUEST_NEW = 0x01
64 65 FLAG_COMMAND_REQUEST_CONTINUATION = 0x02
65 66 FLAG_COMMAND_REQUEST_MORE_FRAMES = 0x04
66 67 FLAG_COMMAND_REQUEST_EXPECT_DATA = 0x08
67 68
68 69 FLAGS_COMMAND_REQUEST = {
69 70 b'new': FLAG_COMMAND_REQUEST_NEW,
70 71 b'continuation': FLAG_COMMAND_REQUEST_CONTINUATION,
71 72 b'more': FLAG_COMMAND_REQUEST_MORE_FRAMES,
72 73 b'have-data': FLAG_COMMAND_REQUEST_EXPECT_DATA,
73 74 }
74 75
75 76 FLAG_COMMAND_DATA_CONTINUATION = 0x01
76 77 FLAG_COMMAND_DATA_EOS = 0x02
77 78
78 79 FLAGS_COMMAND_DATA = {
79 80 b'continuation': FLAG_COMMAND_DATA_CONTINUATION,
80 81 b'eos': FLAG_COMMAND_DATA_EOS,
81 82 }
82 83
83 84 FLAG_COMMAND_RESPONSE_CONTINUATION = 0x01
84 85 FLAG_COMMAND_RESPONSE_EOS = 0x02
85 86
86 87 FLAGS_COMMAND_RESPONSE = {
87 88 b'continuation': FLAG_COMMAND_RESPONSE_CONTINUATION,
88 89 b'eos': FLAG_COMMAND_RESPONSE_EOS,
89 90 }
90 91
91 92 # Maps frame types to their available flags.
92 93 FRAME_TYPE_FLAGS = {
93 94 FRAME_TYPE_COMMAND_REQUEST: FLAGS_COMMAND_REQUEST,
94 95 FRAME_TYPE_COMMAND_DATA: FLAGS_COMMAND_DATA,
95 96 FRAME_TYPE_COMMAND_RESPONSE: FLAGS_COMMAND_RESPONSE,
96 97 FRAME_TYPE_ERROR_RESPONSE: {},
97 98 FRAME_TYPE_TEXT_OUTPUT: {},
98 99 FRAME_TYPE_PROGRESS: {},
99 100 FRAME_TYPE_STREAM_SETTINGS: {},
100 101 }
101 102
102 103 ARGUMENT_RECORD_HEADER = struct.Struct(r'<HH')
103 104
104 105 def humanflags(mapping, value):
105 106 """Convert a numeric flags value to a human value, using a mapping table."""
106 107 namemap = {v: k for k, v in mapping.iteritems()}
107 108 flags = []
108 109 val = 1
109 110 while value >= val:
110 111 if value & val:
111 112 flags.append(namemap.get(val, '<unknown 0x%02x>' % val))
112 113 val <<= 1
113 114
114 115 return b'|'.join(flags)
115 116
116 117 @attr.s(slots=True)
117 118 class frameheader(object):
118 119 """Represents the data in a frame header."""
119 120
120 121 length = attr.ib()
121 122 requestid = attr.ib()
122 123 streamid = attr.ib()
123 124 streamflags = attr.ib()
124 125 typeid = attr.ib()
125 126 flags = attr.ib()
126 127
127 128 @attr.s(slots=True, repr=False)
128 129 class frame(object):
129 130 """Represents a parsed frame."""
130 131
131 132 requestid = attr.ib()
132 133 streamid = attr.ib()
133 134 streamflags = attr.ib()
134 135 typeid = attr.ib()
135 136 flags = attr.ib()
136 137 payload = attr.ib()
137 138
138 139 @encoding.strmethod
139 140 def __repr__(self):
140 141 typename = '<unknown 0x%02x>' % self.typeid
141 142 for name, value in FRAME_TYPES.iteritems():
142 143 if value == self.typeid:
143 144 typename = name
144 145 break
145 146
146 147 return ('frame(size=%d; request=%d; stream=%d; streamflags=%s; '
147 148 'type=%s; flags=%s)' % (
148 149 len(self.payload), self.requestid, self.streamid,
149 150 humanflags(STREAM_FLAGS, self.streamflags), typename,
150 151 humanflags(FRAME_TYPE_FLAGS.get(self.typeid, {}), self.flags)))
151 152
152 153 def makeframe(requestid, streamid, streamflags, typeid, flags, payload):
153 154 """Assemble a frame into a byte array."""
154 155 # TODO assert size of payload.
155 156 frame = bytearray(FRAME_HEADER_SIZE + len(payload))
156 157
157 158 # 24 bits length
158 159 # 16 bits request id
159 160 # 8 bits stream id
160 161 # 8 bits stream flags
161 162 # 4 bits type
162 163 # 4 bits flags
163 164
164 165 l = struct.pack(r'<I', len(payload))
165 166 frame[0:3] = l[0:3]
166 167 struct.pack_into(r'<HBB', frame, 3, requestid, streamid, streamflags)
167 168 frame[7] = (typeid << 4) | flags
168 169 frame[8:] = payload
169 170
170 171 return frame
171 172
172 173 def makeframefromhumanstring(s):
173 174 """Create a frame from a human readable string
174 175
175 176 Strings have the form:
176 177
177 178 <request-id> <stream-id> <stream-flags> <type> <flags> <payload>
178 179
179 180 This can be used by user-facing applications and tests for creating
180 181 frames easily without having to type out a bunch of constants.
181 182
182 183 Request ID and stream IDs are integers.
183 184
184 185 Stream flags, frame type, and flags can be specified by integer or
185 186 named constant.
186 187
187 188 Flags can be delimited by `|` to bitwise OR them together.
188 189
189 190 If the payload begins with ``cbor:``, the following string will be
190 191 evaluated as Python literal and the resulting object will be fed into
191 192 a CBOR encoder. Otherwise, the payload is interpreted as a Python
192 193 byte string literal.
193 194 """
194 195 fields = s.split(b' ', 5)
195 196 requestid, streamid, streamflags, frametype, frameflags, payload = fields
196 197
197 198 requestid = int(requestid)
198 199 streamid = int(streamid)
199 200
200 201 finalstreamflags = 0
201 202 for flag in streamflags.split(b'|'):
202 203 if flag in STREAM_FLAGS:
203 204 finalstreamflags |= STREAM_FLAGS[flag]
204 205 else:
205 206 finalstreamflags |= int(flag)
206 207
207 208 if frametype in FRAME_TYPES:
208 209 frametype = FRAME_TYPES[frametype]
209 210 else:
210 211 frametype = int(frametype)
211 212
212 213 finalflags = 0
213 214 validflags = FRAME_TYPE_FLAGS[frametype]
214 215 for flag in frameflags.split(b'|'):
215 216 if flag in validflags:
216 217 finalflags |= validflags[flag]
217 218 else:
218 219 finalflags |= int(flag)
219 220
220 221 if payload.startswith(b'cbor:'):
221 222 payload = b''.join(cborutil.streamencode(
222 223 stringutil.evalpythonliteral(payload[5:])))
223 224
224 225 else:
225 226 payload = stringutil.unescapestr(payload)
226 227
227 228 return makeframe(requestid=requestid, streamid=streamid,
228 229 streamflags=finalstreamflags, typeid=frametype,
229 230 flags=finalflags, payload=payload)
230 231
231 232 def parseheader(data):
232 233 """Parse a unified framing protocol frame header from a buffer.
233 234
234 235 The header is expected to be in the buffer at offset 0 and the
235 236 buffer is expected to be large enough to hold a full header.
236 237 """
237 238 # 24 bits payload length (little endian)
238 239 # 16 bits request ID
239 240 # 8 bits stream ID
240 241 # 8 bits stream flags
241 242 # 4 bits frame type
242 243 # 4 bits frame flags
243 244 # ... payload
244 245 framelength = data[0] + 256 * data[1] + 16384 * data[2]
245 246 requestid, streamid, streamflags = struct.unpack_from(r'<HBB', data, 3)
246 247 typeflags = data[7]
247 248
248 249 frametype = (typeflags & 0xf0) >> 4
249 250 frameflags = typeflags & 0x0f
250 251
251 252 return frameheader(framelength, requestid, streamid, streamflags,
252 253 frametype, frameflags)
253 254
254 255 def readframe(fh):
255 256 """Read a unified framing protocol frame from a file object.
256 257
257 258 Returns a 3-tuple of (type, flags, payload) for the decoded frame or
258 259 None if no frame is available. May raise if a malformed frame is
259 260 seen.
260 261 """
261 262 header = bytearray(FRAME_HEADER_SIZE)
262 263
263 264 readcount = fh.readinto(header)
264 265
265 266 if readcount == 0:
266 267 return None
267 268
268 269 if readcount != FRAME_HEADER_SIZE:
269 270 raise error.Abort(_('received incomplete frame: got %d bytes: %s') %
270 271 (readcount, header))
271 272
272 273 h = parseheader(header)
273 274
274 275 payload = fh.read(h.length)
275 276 if len(payload) != h.length:
276 277 raise error.Abort(_('frame length error: expected %d; got %d') %
277 278 (h.length, len(payload)))
278 279
279 280 return frame(h.requestid, h.streamid, h.streamflags, h.typeid, h.flags,
280 281 payload)
281 282
282 283 def createcommandframes(stream, requestid, cmd, args, datafh=None,
283 284 maxframesize=DEFAULT_MAX_FRAME_SIZE,
284 285 redirect=None):
285 286 """Create frames necessary to transmit a request to run a command.
286 287
287 288 This is a generator of bytearrays. Each item represents a frame
288 289 ready to be sent over the wire to a peer.
289 290 """
290 291 data = {b'name': cmd}
291 292 if args:
292 293 data[b'args'] = args
293 294
294 295 if redirect:
295 296 data[b'redirect'] = redirect
296 297
297 298 data = b''.join(cborutil.streamencode(data))
298 299
299 300 offset = 0
300 301
301 302 while True:
302 303 flags = 0
303 304
304 305 # Must set new or continuation flag.
305 306 if not offset:
306 307 flags |= FLAG_COMMAND_REQUEST_NEW
307 308 else:
308 309 flags |= FLAG_COMMAND_REQUEST_CONTINUATION
309 310
310 311 # Data frames is set on all frames.
311 312 if datafh:
312 313 flags |= FLAG_COMMAND_REQUEST_EXPECT_DATA
313 314
314 315 payload = data[offset:offset + maxframesize]
315 316 offset += len(payload)
316 317
317 318 if len(payload) == maxframesize and offset < len(data):
318 319 flags |= FLAG_COMMAND_REQUEST_MORE_FRAMES
319 320
320 321 yield stream.makeframe(requestid=requestid,
321 322 typeid=FRAME_TYPE_COMMAND_REQUEST,
322 323 flags=flags,
323 324 payload=payload)
324 325
325 326 if not (flags & FLAG_COMMAND_REQUEST_MORE_FRAMES):
326 327 break
327 328
328 329 if datafh:
329 330 while True:
330 331 data = datafh.read(DEFAULT_MAX_FRAME_SIZE)
331 332
332 333 done = False
333 334 if len(data) == DEFAULT_MAX_FRAME_SIZE:
334 335 flags = FLAG_COMMAND_DATA_CONTINUATION
335 336 else:
336 337 flags = FLAG_COMMAND_DATA_EOS
337 338 assert datafh.read(1) == b''
338 339 done = True
339 340
340 341 yield stream.makeframe(requestid=requestid,
341 342 typeid=FRAME_TYPE_COMMAND_DATA,
342 343 flags=flags,
343 344 payload=data)
344 345
345 346 if done:
346 347 break
347 348
348 349 def createcommandresponseframesfrombytes(stream, requestid, data,
349 350 maxframesize=DEFAULT_MAX_FRAME_SIZE):
350 351 """Create a raw frame to send a bytes response from static bytes input.
351 352
352 353 Returns a generator of bytearrays.
353 354 """
354 355 # Automatically send the overall CBOR response map.
355 356 overall = b''.join(cborutil.streamencode({b'status': b'ok'}))
356 357 if len(overall) > maxframesize:
357 358 raise error.ProgrammingError('not yet implemented')
358 359
359 360 # Simple case where we can fit the full response in a single frame.
360 361 if len(overall) + len(data) <= maxframesize:
361 362 flags = FLAG_COMMAND_RESPONSE_EOS
362 363 yield stream.makeframe(requestid=requestid,
363 364 typeid=FRAME_TYPE_COMMAND_RESPONSE,
364 365 flags=flags,
365 366 payload=overall + data)
366 367 return
367 368
368 369 # It's easier to send the overall CBOR map in its own frame than to track
369 370 # offsets.
370 371 yield stream.makeframe(requestid=requestid,
371 372 typeid=FRAME_TYPE_COMMAND_RESPONSE,
372 373 flags=FLAG_COMMAND_RESPONSE_CONTINUATION,
373 374 payload=overall)
374 375
375 376 offset = 0
376 377 while True:
377 378 chunk = data[offset:offset + maxframesize]
378 379 offset += len(chunk)
379 380 done = offset == len(data)
380 381
381 382 if done:
382 383 flags = FLAG_COMMAND_RESPONSE_EOS
383 384 else:
384 385 flags = FLAG_COMMAND_RESPONSE_CONTINUATION
385 386
386 387 yield stream.makeframe(requestid=requestid,
387 388 typeid=FRAME_TYPE_COMMAND_RESPONSE,
388 389 flags=flags,
389 390 payload=chunk)
390 391
391 392 if done:
392 393 break
393 394
394 395 def createbytesresponseframesfromgen(stream, requestid, gen,
395 396 maxframesize=DEFAULT_MAX_FRAME_SIZE):
396 397 """Generator of frames from a generator of byte chunks.
397 398
398 399 This assumes that another frame will follow whatever this emits. i.e.
399 400 this always emits the continuation flag and never emits the end-of-stream
400 401 flag.
401 402 """
402 403 cb = util.chunkbuffer(gen)
403 404 flags = FLAG_COMMAND_RESPONSE_CONTINUATION
404 405
405 406 while True:
406 407 chunk = cb.read(maxframesize)
407 408 if not chunk:
408 409 break
409 410
410 411 yield stream.makeframe(requestid=requestid,
411 412 typeid=FRAME_TYPE_COMMAND_RESPONSE,
412 413 flags=flags,
413 414 payload=chunk)
414 415
415 416 flags |= FLAG_COMMAND_RESPONSE_CONTINUATION
416 417
417 418 def createcommandresponseokframe(stream, requestid):
418 419 overall = b''.join(cborutil.streamencode({b'status': b'ok'}))
419 420
420 421 return stream.makeframe(requestid=requestid,
421 422 typeid=FRAME_TYPE_COMMAND_RESPONSE,
422 423 flags=FLAG_COMMAND_RESPONSE_CONTINUATION,
423 424 payload=overall)
424 425
425 426 def createcommandresponseeosframe(stream, requestid):
426 427 """Create an empty payload frame representing command end-of-stream."""
427 428 return stream.makeframe(requestid=requestid,
428 429 typeid=FRAME_TYPE_COMMAND_RESPONSE,
429 430 flags=FLAG_COMMAND_RESPONSE_EOS,
430 431 payload=b'')
431 432
433 def createalternatelocationresponseframe(stream, requestid, location):
434 data = {
435 b'status': b'redirect',
436 b'location': {
437 b'url': location.url,
438 b'mediatype': location.mediatype,
439 }
440 }
441
442 for a in (r'size', r'fullhashes', r'fullhashseed', r'serverdercerts',
443 r'servercadercerts'):
444 value = getattr(location, a)
445 if value is not None:
446 data[b'location'][pycompat.bytestr(a)] = value
447
448 return stream.makeframe(requestid=requestid,
449 typeid=FRAME_TYPE_COMMAND_RESPONSE,
450 flags=FLAG_COMMAND_RESPONSE_CONTINUATION,
451 payload=b''.join(cborutil.streamencode(data)))
452
432 453 def createcommanderrorresponse(stream, requestid, message, args=None):
433 454 # TODO should this be using a list of {'msg': ..., 'args': {}} so atom
434 455 # formatting works consistently?
435 456 m = {
436 457 b'status': b'error',
437 458 b'error': {
438 459 b'message': message,
439 460 }
440 461 }
441 462
442 463 if args:
443 464 m[b'error'][b'args'] = args
444 465
445 466 overall = b''.join(cborutil.streamencode(m))
446 467
447 468 yield stream.makeframe(requestid=requestid,
448 469 typeid=FRAME_TYPE_COMMAND_RESPONSE,
449 470 flags=FLAG_COMMAND_RESPONSE_EOS,
450 471 payload=overall)
451 472
452 473 def createerrorframe(stream, requestid, msg, errtype):
453 474 # TODO properly handle frame size limits.
454 475 assert len(msg) <= DEFAULT_MAX_FRAME_SIZE
455 476
456 477 payload = b''.join(cborutil.streamencode({
457 478 b'type': errtype,
458 479 b'message': [{b'msg': msg}],
459 480 }))
460 481
461 482 yield stream.makeframe(requestid=requestid,
462 483 typeid=FRAME_TYPE_ERROR_RESPONSE,
463 484 flags=0,
464 485 payload=payload)
465 486
466 487 def createtextoutputframe(stream, requestid, atoms,
467 488 maxframesize=DEFAULT_MAX_FRAME_SIZE):
468 489 """Create a text output frame to render text to people.
469 490
470 491 ``atoms`` is a 3-tuple of (formatting string, args, labels).
471 492
472 493 The formatting string contains ``%s`` tokens to be replaced by the
473 494 corresponding indexed entry in ``args``. ``labels`` is an iterable of
474 495 formatters to be applied at rendering time. In terms of the ``ui``
475 496 class, each atom corresponds to a ``ui.write()``.
476 497 """
477 498 atomdicts = []
478 499
479 500 for (formatting, args, labels) in atoms:
480 501 # TODO look for localstr, other types here?
481 502
482 503 if not isinstance(formatting, bytes):
483 504 raise ValueError('must use bytes formatting strings')
484 505 for arg in args:
485 506 if not isinstance(arg, bytes):
486 507 raise ValueError('must use bytes for arguments')
487 508 for label in labels:
488 509 if not isinstance(label, bytes):
489 510 raise ValueError('must use bytes for labels')
490 511
491 512 # Formatting string must be ASCII.
492 513 formatting = formatting.decode(r'ascii', r'replace').encode(r'ascii')
493 514
494 515 # Arguments must be UTF-8.
495 516 args = [a.decode(r'utf-8', r'replace').encode(r'utf-8') for a in args]
496 517
497 518 # Labels must be ASCII.
498 519 labels = [l.decode(r'ascii', r'strict').encode(r'ascii')
499 520 for l in labels]
500 521
501 522 atom = {b'msg': formatting}
502 523 if args:
503 524 atom[b'args'] = args
504 525 if labels:
505 526 atom[b'labels'] = labels
506 527
507 528 atomdicts.append(atom)
508 529
509 530 payload = b''.join(cborutil.streamencode(atomdicts))
510 531
511 532 if len(payload) > maxframesize:
512 533 raise ValueError('cannot encode data in a single frame')
513 534
514 535 yield stream.makeframe(requestid=requestid,
515 536 typeid=FRAME_TYPE_TEXT_OUTPUT,
516 537 flags=0,
517 538 payload=payload)
518 539
519 540 class bufferingcommandresponseemitter(object):
520 541 """Helper object to emit command response frames intelligently.
521 542
522 543 Raw command response data is likely emitted in chunks much smaller
523 544 than what can fit in a single frame. This class exists to buffer
524 545 chunks until enough data is available to fit in a single frame.
525 546
526 547 TODO we'll need something like this when compression is supported.
527 548 So it might make sense to implement this functionality at the stream
528 549 level.
529 550 """
530 551 def __init__(self, stream, requestid, maxframesize=DEFAULT_MAX_FRAME_SIZE):
531 552 self._stream = stream
532 553 self._requestid = requestid
533 554 self._maxsize = maxframesize
534 555 self._chunks = []
535 556 self._chunkssize = 0
536 557
537 558 def send(self, data):
538 559 """Send new data for emission.
539 560
540 561 Is a generator of new frames that were derived from the new input.
541 562
542 563 If the special input ``None`` is received, flushes all buffered
543 564 data to frames.
544 565 """
545 566
546 567 if data is None:
547 568 for frame in self._flush():
548 569 yield frame
549 570 return
550 571
551 572 # There is a ton of potential to do more complicated things here.
552 573 # Our immediate goal is to coalesce small chunks into big frames,
553 574 # not achieve the fewest number of frames possible. So we go with
554 575 # a simple implementation:
555 576 #
556 577 # * If a chunk is too large for a frame, we flush and emit frames
557 578 # for the new chunk.
558 579 # * If a chunk can be buffered without total buffered size limits
559 580 # being exceeded, we do that.
560 581 # * If a chunk causes us to go over our buffering limit, we flush
561 582 # and then buffer the new chunk.
562 583
563 584 if len(data) > self._maxsize:
564 585 for frame in self._flush():
565 586 yield frame
566 587
567 588 # Now emit frames for the big chunk.
568 589 offset = 0
569 590 while True:
570 591 chunk = data[offset:offset + self._maxsize]
571 592 offset += len(chunk)
572 593
573 594 yield self._stream.makeframe(
574 595 self._requestid,
575 596 typeid=FRAME_TYPE_COMMAND_RESPONSE,
576 597 flags=FLAG_COMMAND_RESPONSE_CONTINUATION,
577 598 payload=chunk)
578 599
579 600 if offset == len(data):
580 601 return
581 602
582 603 # If we don't have enough to constitute a full frame, buffer and
583 604 # return.
584 605 if len(data) + self._chunkssize < self._maxsize:
585 606 self._chunks.append(data)
586 607 self._chunkssize += len(data)
587 608 return
588 609
589 610 # Else flush what we have and buffer the new chunk. We could do
590 611 # something more intelligent here, like break the chunk. Let's
591 612 # keep things simple for now.
592 613 for frame in self._flush():
593 614 yield frame
594 615
595 616 self._chunks.append(data)
596 617 self._chunkssize = len(data)
597 618
598 619 def _flush(self):
599 620 payload = b''.join(self._chunks)
600 621 assert len(payload) <= self._maxsize
601 622
602 623 self._chunks[:] = []
603 624 self._chunkssize = 0
604 625
605 626 yield self._stream.makeframe(
606 627 self._requestid,
607 628 typeid=FRAME_TYPE_COMMAND_RESPONSE,
608 629 flags=FLAG_COMMAND_RESPONSE_CONTINUATION,
609 630 payload=payload)
610 631
611 632 class stream(object):
612 633 """Represents a logical unidirectional series of frames."""
613 634
614 635 def __init__(self, streamid, active=False):
615 636 self.streamid = streamid
616 637 self._active = active
617 638
618 639 def makeframe(self, requestid, typeid, flags, payload):
619 640 """Create a frame to be sent out over this stream.
620 641
621 642 Only returns the frame instance. Does not actually send it.
622 643 """
623 644 streamflags = 0
624 645 if not self._active:
625 646 streamflags |= STREAM_FLAG_BEGIN_STREAM
626 647 self._active = True
627 648
628 649 return makeframe(requestid, self.streamid, streamflags, typeid, flags,
629 650 payload)
630 651
631 652 def ensureserverstream(stream):
632 653 if stream.streamid % 2:
633 654 raise error.ProgrammingError('server should only write to even '
634 655 'numbered streams; %d is not even' %
635 656 stream.streamid)
636 657
637 658 class serverreactor(object):
638 659 """Holds state of a server handling frame-based protocol requests.
639 660
640 661 This class is the "brain" of the unified frame-based protocol server
641 662 component. While the protocol is stateless from the perspective of
642 663 requests/commands, something needs to track which frames have been
643 664 received, what frames to expect, etc. This class is that thing.
644 665
645 666 Instances are modeled as a state machine of sorts. Instances are also
646 667 reactionary to external events. The point of this class is to encapsulate
647 668 the state of the connection and the exchange of frames, not to perform
648 669 work. Instead, callers tell this class when something occurs, like a
649 670 frame arriving. If that activity is worthy of a follow-up action (say
650 671 *run a command*), the return value of that handler will say so.
651 672
652 673 I/O and CPU intensive operations are purposefully delegated outside of
653 674 this class.
654 675
655 676 Consumers are expected to tell instances when events occur. They do so by
656 677 calling the various ``on*`` methods. These methods return a 2-tuple
657 678 describing any follow-up action(s) to take. The first element is the
658 679 name of an action to perform. The second is a data structure (usually
659 680 a dict) specific to that action that contains more information. e.g.
660 681 if the server wants to send frames back to the client, the data structure
661 682 will contain a reference to those frames.
662 683
663 684 Valid actions that consumers can be instructed to take are:
664 685
665 686 sendframes
666 687 Indicates that frames should be sent to the client. The ``framegen``
667 688 key contains a generator of frames that should be sent. The server
668 689 assumes that all frames are sent to the client.
669 690
670 691 error
671 692 Indicates that an error occurred. Consumer should probably abort.
672 693
673 694 runcommand
674 695 Indicates that the consumer should run a wire protocol command. Details
675 696 of the command to run are given in the data structure.
676 697
677 698 wantframe
678 699 Indicates that nothing of interest happened and the server is waiting on
679 700 more frames from the client before anything interesting can be done.
680 701
681 702 noop
682 703 Indicates no additional action is required.
683 704
684 705 Known Issues
685 706 ------------
686 707
687 708 There are no limits to the number of partially received commands or their
688 709 size. A malicious client could stream command request data and exhaust the
689 710 server's memory.
690 711
691 712 Partially received commands are not acted upon when end of input is
692 713 reached. Should the server error if it receives a partial request?
693 714 Should the client send a message to abort a partially transmitted request
694 715 to facilitate graceful shutdown?
695 716
696 717 Active requests that haven't been responded to aren't tracked. This means
697 718 that if we receive a command and instruct its dispatch, another command
698 719 with its request ID can come in over the wire and there will be a race
699 720 between who responds to what.
700 721 """
701 722
702 723 def __init__(self, deferoutput=False):
703 724 """Construct a new server reactor.
704 725
705 726 ``deferoutput`` can be used to indicate that no output frames should be
706 727 instructed to be sent until input has been exhausted. In this mode,
707 728 events that would normally generate output frames (such as a command
708 729 response being ready) will instead defer instructing the consumer to
709 730 send those frames. This is useful for half-duplex transports where the
710 731 sender cannot receive until all data has been transmitted.
711 732 """
712 733 self._deferoutput = deferoutput
713 734 self._state = 'idle'
714 735 self._nextoutgoingstreamid = 2
715 736 self._bufferedframegens = []
716 737 # stream id -> stream instance for all active streams from the client.
717 738 self._incomingstreams = {}
718 739 self._outgoingstreams = {}
719 740 # request id -> dict of commands that are actively being received.
720 741 self._receivingcommands = {}
721 742 # Request IDs that have been received and are actively being processed.
722 743 # Once all output for a request has been sent, it is removed from this
723 744 # set.
724 745 self._activecommands = set()
725 746
726 747 def onframerecv(self, frame):
727 748 """Process a frame that has been received off the wire.
728 749
729 750 Returns a dict with an ``action`` key that details what action,
730 751 if any, the consumer should take next.
731 752 """
732 753 if not frame.streamid % 2:
733 754 self._state = 'errored'
734 755 return self._makeerrorresult(
735 756 _('received frame with even numbered stream ID: %d') %
736 757 frame.streamid)
737 758
738 759 if frame.streamid not in self._incomingstreams:
739 760 if not frame.streamflags & STREAM_FLAG_BEGIN_STREAM:
740 761 self._state = 'errored'
741 762 return self._makeerrorresult(
742 763 _('received frame on unknown inactive stream without '
743 764 'beginning of stream flag set'))
744 765
745 766 self._incomingstreams[frame.streamid] = stream(frame.streamid)
746 767
747 768 if frame.streamflags & STREAM_FLAG_ENCODING_APPLIED:
748 769 # TODO handle decoding frames
749 770 self._state = 'errored'
750 771 raise error.ProgrammingError('support for decoding stream payloads '
751 772 'not yet implemented')
752 773
753 774 if frame.streamflags & STREAM_FLAG_END_STREAM:
754 775 del self._incomingstreams[frame.streamid]
755 776
756 777 handlers = {
757 778 'idle': self._onframeidle,
758 779 'command-receiving': self._onframecommandreceiving,
759 780 'errored': self._onframeerrored,
760 781 }
761 782
762 783 meth = handlers.get(self._state)
763 784 if not meth:
764 785 raise error.ProgrammingError('unhandled state: %s' % self._state)
765 786
766 787 return meth(frame)
767 788
768 789 def oncommandresponseready(self, stream, requestid, data):
769 790 """Signal that a bytes response is ready to be sent to the client.
770 791
771 792 The raw bytes response is passed as an argument.
772 793 """
773 794 ensureserverstream(stream)
774 795
775 796 def sendframes():
776 797 for frame in createcommandresponseframesfrombytes(stream, requestid,
777 798 data):
778 799 yield frame
779 800
780 801 self._activecommands.remove(requestid)
781 802
782 803 result = sendframes()
783 804
784 805 if self._deferoutput:
785 806 self._bufferedframegens.append(result)
786 807 return 'noop', {}
787 808 else:
788 809 return 'sendframes', {
789 810 'framegen': result,
790 811 }
791 812
792 813 def oncommandresponsereadyobjects(self, stream, requestid, objs):
793 814 """Signal that objects are ready to be sent to the client.
794 815
795 816 ``objs`` is an iterable of objects (typically a generator) that will
796 817 be encoded via CBOR and added to frames, which will be sent to the
797 818 client.
798 819 """
799 820 ensureserverstream(stream)
800 821
801 822 # We need to take care over exception handling. Uncaught exceptions
802 823 # when generating frames could lead to premature end of the frame
803 824 # stream and the possibility of the server or client process getting
804 825 # in a bad state.
805 826 #
806 827 # Keep in mind that if ``objs`` is a generator, advancing it could
807 828 # raise exceptions that originated in e.g. wire protocol command
808 829 # functions. That is why we differentiate between exceptions raised
809 830 # when iterating versus other exceptions that occur.
810 831 #
811 832 # In all cases, when the function finishes, the request is fully
812 833 # handled and no new frames for it should be seen.
813 834
814 835 def sendframes():
815 836 emitted = False
837 alternatelocationsent = False
816 838 emitter = bufferingcommandresponseemitter(stream, requestid)
817 839 while True:
818 840 try:
819 841 o = next(objs)
820 842 except StopIteration:
821 843 for frame in emitter.send(None):
822 844 yield frame
823 845
824 846 if emitted:
825 847 yield createcommandresponseeosframe(stream, requestid)
826 848 break
827 849
828 850 except error.WireprotoCommandError as e:
829 851 for frame in createcommanderrorresponse(
830 852 stream, requestid, e.message, e.messageargs):
831 853 yield frame
832 854 break
833 855
834 856 except Exception as e:
835 857 for frame in createerrorframe(
836 858 stream, requestid, '%s' % stringutil.forcebytestr(e),
837 859 errtype='server'):
838 860
839 861 yield frame
840 862
841 863 break
842 864
843 865 try:
866 # Alternate location responses can only be the first and
867 # only object in the output stream.
868 if isinstance(o, wireprototypes.alternatelocationresponse):
869 if emitted:
870 raise error.ProgrammingError(
871 'alternatelocationresponse seen after initial '
872 'output object')
873
874 yield createalternatelocationresponseframe(
875 stream, requestid, o)
876
877 alternatelocationsent = True
878 emitted = True
879 continue
880
881 if alternatelocationsent:
882 raise error.ProgrammingError(
883 'object follows alternatelocationresponse')
884
844 885 if not emitted:
845 886 yield createcommandresponseokframe(stream, requestid)
846 887 emitted = True
847 888
848 889 # Objects emitted by command functions can be serializable
849 890 # data structures or special types.
850 891 # TODO consider extracting the content normalization to a
851 892 # standalone function, as it may be useful for e.g. cachers.
852 893
853 894 # A pre-encoded object is sent directly to the emitter.
854 895 if isinstance(o, wireprototypes.encodedresponse):
855 896 for frame in emitter.send(o.data):
856 897 yield frame
857 898
858 899 # A regular object is CBOR encoded.
859 900 else:
860 901 for chunk in cborutil.streamencode(o):
861 902 for frame in emitter.send(chunk):
862 903 yield frame
863 904
864 905 except Exception as e:
865 906 for frame in createerrorframe(stream, requestid,
866 907 '%s' % e,
867 908 errtype='server'):
868 909 yield frame
869 910
870 911 break
871 912
872 913 self._activecommands.remove(requestid)
873 914
874 915 return self._handlesendframes(sendframes())
875 916
876 917 def oninputeof(self):
877 918 """Signals that end of input has been received.
878 919
879 920 No more frames will be received. All pending activity should be
880 921 completed.
881 922 """
882 923 # TODO should we do anything about in-flight commands?
883 924
884 925 if not self._deferoutput or not self._bufferedframegens:
885 926 return 'noop', {}
886 927
887 928 # If we buffered all our responses, emit those.
888 929 def makegen():
889 930 for gen in self._bufferedframegens:
890 931 for frame in gen:
891 932 yield frame
892 933
893 934 return 'sendframes', {
894 935 'framegen': makegen(),
895 936 }
896 937
897 938 def _handlesendframes(self, framegen):
898 939 if self._deferoutput:
899 940 self._bufferedframegens.append(framegen)
900 941 return 'noop', {}
901 942 else:
902 943 return 'sendframes', {
903 944 'framegen': framegen,
904 945 }
905 946
906 947 def onservererror(self, stream, requestid, msg):
907 948 ensureserverstream(stream)
908 949
909 950 def sendframes():
910 951 for frame in createerrorframe(stream, requestid, msg,
911 952 errtype='server'):
912 953 yield frame
913 954
914 955 self._activecommands.remove(requestid)
915 956
916 957 return self._handlesendframes(sendframes())
917 958
918 959 def oncommanderror(self, stream, requestid, message, args=None):
919 960 """Called when a command encountered an error before sending output."""
920 961 ensureserverstream(stream)
921 962
922 963 def sendframes():
923 964 for frame in createcommanderrorresponse(stream, requestid, message,
924 965 args):
925 966 yield frame
926 967
927 968 self._activecommands.remove(requestid)
928 969
929 970 return self._handlesendframes(sendframes())
930 971
931 972 def makeoutputstream(self):
932 973 """Create a stream to be used for sending data to the client."""
933 974 streamid = self._nextoutgoingstreamid
934 975 self._nextoutgoingstreamid += 2
935 976
936 977 s = stream(streamid)
937 978 self._outgoingstreams[streamid] = s
938 979
939 980 return s
940 981
941 982 def _makeerrorresult(self, msg):
942 983 return 'error', {
943 984 'message': msg,
944 985 }
945 986
946 987 def _makeruncommandresult(self, requestid):
947 988 entry = self._receivingcommands[requestid]
948 989
949 990 if not entry['requestdone']:
950 991 self._state = 'errored'
951 992 raise error.ProgrammingError('should not be called without '
952 993 'requestdone set')
953 994
954 995 del self._receivingcommands[requestid]
955 996
956 997 if self._receivingcommands:
957 998 self._state = 'command-receiving'
958 999 else:
959 1000 self._state = 'idle'
960 1001
961 1002 # Decode the payloads as CBOR.
962 1003 entry['payload'].seek(0)
963 1004 request = cborutil.decodeall(entry['payload'].getvalue())[0]
964 1005
965 1006 if b'name' not in request:
966 1007 self._state = 'errored'
967 1008 return self._makeerrorresult(
968 1009 _('command request missing "name" field'))
969 1010
970 1011 if b'args' not in request:
971 1012 request[b'args'] = {}
972 1013
973 1014 assert requestid not in self._activecommands
974 1015 self._activecommands.add(requestid)
975 1016
976 1017 return 'runcommand', {
977 1018 'requestid': requestid,
978 1019 'command': request[b'name'],
979 1020 'args': request[b'args'],
1021 'redirect': request.get(b'redirect'),
980 1022 'data': entry['data'].getvalue() if entry['data'] else None,
981 1023 }
982 1024
983 1025 def _makewantframeresult(self):
984 1026 return 'wantframe', {
985 1027 'state': self._state,
986 1028 }
987 1029
988 1030 def _validatecommandrequestframe(self, frame):
989 1031 new = frame.flags & FLAG_COMMAND_REQUEST_NEW
990 1032 continuation = frame.flags & FLAG_COMMAND_REQUEST_CONTINUATION
991 1033
992 1034 if new and continuation:
993 1035 self._state = 'errored'
994 1036 return self._makeerrorresult(
995 1037 _('received command request frame with both new and '
996 1038 'continuation flags set'))
997 1039
998 1040 if not new and not continuation:
999 1041 self._state = 'errored'
1000 1042 return self._makeerrorresult(
1001 1043 _('received command request frame with neither new nor '
1002 1044 'continuation flags set'))
1003 1045
1004 1046 def _onframeidle(self, frame):
1005 1047 # The only frame type that should be received in this state is a
1006 1048 # command request.
1007 1049 if frame.typeid != FRAME_TYPE_COMMAND_REQUEST:
1008 1050 self._state = 'errored'
1009 1051 return self._makeerrorresult(
1010 1052 _('expected command request frame; got %d') % frame.typeid)
1011 1053
1012 1054 res = self._validatecommandrequestframe(frame)
1013 1055 if res:
1014 1056 return res
1015 1057
1016 1058 if frame.requestid in self._receivingcommands:
1017 1059 self._state = 'errored'
1018 1060 return self._makeerrorresult(
1019 1061 _('request with ID %d already received') % frame.requestid)
1020 1062
1021 1063 if frame.requestid in self._activecommands:
1022 1064 self._state = 'errored'
1023 1065 return self._makeerrorresult(
1024 1066 _('request with ID %d is already active') % frame.requestid)
1025 1067
1026 1068 new = frame.flags & FLAG_COMMAND_REQUEST_NEW
1027 1069 moreframes = frame.flags & FLAG_COMMAND_REQUEST_MORE_FRAMES
1028 1070 expectingdata = frame.flags & FLAG_COMMAND_REQUEST_EXPECT_DATA
1029 1071
1030 1072 if not new:
1031 1073 self._state = 'errored'
1032 1074 return self._makeerrorresult(
1033 1075 _('received command request frame without new flag set'))
1034 1076
1035 1077 payload = util.bytesio()
1036 1078 payload.write(frame.payload)
1037 1079
1038 1080 self._receivingcommands[frame.requestid] = {
1039 1081 'payload': payload,
1040 1082 'data': None,
1041 1083 'requestdone': not moreframes,
1042 1084 'expectingdata': bool(expectingdata),
1043 1085 }
1044 1086
1045 1087 # This is the final frame for this request. Dispatch it.
1046 1088 if not moreframes and not expectingdata:
1047 1089 return self._makeruncommandresult(frame.requestid)
1048 1090
1049 1091 assert moreframes or expectingdata
1050 1092 self._state = 'command-receiving'
1051 1093 return self._makewantframeresult()
1052 1094
1053 1095 def _onframecommandreceiving(self, frame):
1054 1096 if frame.typeid == FRAME_TYPE_COMMAND_REQUEST:
1055 1097 # Process new command requests as such.
1056 1098 if frame.flags & FLAG_COMMAND_REQUEST_NEW:
1057 1099 return self._onframeidle(frame)
1058 1100
1059 1101 res = self._validatecommandrequestframe(frame)
1060 1102 if res:
1061 1103 return res
1062 1104
1063 1105 # All other frames should be related to a command that is currently
1064 1106 # receiving but is not active.
1065 1107 if frame.requestid in self._activecommands:
1066 1108 self._state = 'errored'
1067 1109 return self._makeerrorresult(
1068 1110 _('received frame for request that is still active: %d') %
1069 1111 frame.requestid)
1070 1112
1071 1113 if frame.requestid not in self._receivingcommands:
1072 1114 self._state = 'errored'
1073 1115 return self._makeerrorresult(
1074 1116 _('received frame for request that is not receiving: %d') %
1075 1117 frame.requestid)
1076 1118
1077 1119 entry = self._receivingcommands[frame.requestid]
1078 1120
1079 1121 if frame.typeid == FRAME_TYPE_COMMAND_REQUEST:
1080 1122 moreframes = frame.flags & FLAG_COMMAND_REQUEST_MORE_FRAMES
1081 1123 expectingdata = bool(frame.flags & FLAG_COMMAND_REQUEST_EXPECT_DATA)
1082 1124
1083 1125 if entry['requestdone']:
1084 1126 self._state = 'errored'
1085 1127 return self._makeerrorresult(
1086 1128 _('received command request frame when request frames '
1087 1129 'were supposedly done'))
1088 1130
1089 1131 if expectingdata != entry['expectingdata']:
1090 1132 self._state = 'errored'
1091 1133 return self._makeerrorresult(
1092 1134 _('mismatch between expect data flag and previous frame'))
1093 1135
1094 1136 entry['payload'].write(frame.payload)
1095 1137
1096 1138 if not moreframes:
1097 1139 entry['requestdone'] = True
1098 1140
1099 1141 if not moreframes and not expectingdata:
1100 1142 return self._makeruncommandresult(frame.requestid)
1101 1143
1102 1144 return self._makewantframeresult()
1103 1145
1104 1146 elif frame.typeid == FRAME_TYPE_COMMAND_DATA:
1105 1147 if not entry['expectingdata']:
1106 1148 self._state = 'errored'
1107 1149 return self._makeerrorresult(_(
1108 1150 'received command data frame for request that is not '
1109 1151 'expecting data: %d') % frame.requestid)
1110 1152
1111 1153 if entry['data'] is None:
1112 1154 entry['data'] = util.bytesio()
1113 1155
1114 1156 return self._handlecommanddataframe(frame, entry)
1115 1157 else:
1116 1158 self._state = 'errored'
1117 1159 return self._makeerrorresult(_(
1118 1160 'received unexpected frame type: %d') % frame.typeid)
1119 1161
1120 1162 def _handlecommanddataframe(self, frame, entry):
1121 1163 assert frame.typeid == FRAME_TYPE_COMMAND_DATA
1122 1164
1123 1165 # TODO support streaming data instead of buffering it.
1124 1166 entry['data'].write(frame.payload)
1125 1167
1126 1168 if frame.flags & FLAG_COMMAND_DATA_CONTINUATION:
1127 1169 return self._makewantframeresult()
1128 1170 elif frame.flags & FLAG_COMMAND_DATA_EOS:
1129 1171 entry['data'].seek(0)
1130 1172 return self._makeruncommandresult(frame.requestid)
1131 1173 else:
1132 1174 self._state = 'errored'
1133 1175 return self._makeerrorresult(_('command data frame without '
1134 1176 'flags'))
1135 1177
1136 1178 def _onframeerrored(self, frame):
1137 1179 return self._makeerrorresult(_('server already errored'))
1138 1180
1139 1181 class commandrequest(object):
1140 1182 """Represents a request to run a command."""
1141 1183
1142 1184 def __init__(self, requestid, name, args, datafh=None, redirect=None):
1143 1185 self.requestid = requestid
1144 1186 self.name = name
1145 1187 self.args = args
1146 1188 self.datafh = datafh
1147 1189 self.redirect = redirect
1148 1190 self.state = 'pending'
1149 1191
1150 1192 class clientreactor(object):
1151 1193 """Holds state of a client issuing frame-based protocol requests.
1152 1194
1153 1195 This is like ``serverreactor`` but for client-side state.
1154 1196
1155 1197 Each instance is bound to the lifetime of a connection. For persistent
1156 1198 connection transports using e.g. TCP sockets and speaking the raw
1157 1199 framing protocol, there will be a single instance for the lifetime of
1158 1200 the TCP socket. For transports where there are multiple discrete
1159 1201 interactions (say tunneled within in HTTP request), there will be a
1160 1202 separate instance for each distinct interaction.
1161 1203 """
1162 1204 def __init__(self, hasmultiplesend=False, buffersends=True):
1163 1205 """Create a new instance.
1164 1206
1165 1207 ``hasmultiplesend`` indicates whether multiple sends are supported
1166 1208 by the transport. When True, it is possible to send commands immediately
1167 1209 instead of buffering until the caller signals an intent to finish a
1168 1210 send operation.
1169 1211
1170 1212 ``buffercommands`` indicates whether sends should be buffered until the
1171 1213 last request has been issued.
1172 1214 """
1173 1215 self._hasmultiplesend = hasmultiplesend
1174 1216 self._buffersends = buffersends
1175 1217
1176 1218 self._canissuecommands = True
1177 1219 self._cansend = True
1178 1220
1179 1221 self._nextrequestid = 1
1180 1222 # We only support a single outgoing stream for now.
1181 1223 self._outgoingstream = stream(1)
1182 1224 self._pendingrequests = collections.deque()
1183 1225 self._activerequests = {}
1184 1226 self._incomingstreams = {}
1185 1227
1186 1228 def callcommand(self, name, args, datafh=None, redirect=None):
1187 1229 """Request that a command be executed.
1188 1230
1189 1231 Receives the command name, a dict of arguments to pass to the command,
1190 1232 and an optional file object containing the raw data for the command.
1191 1233
1192 1234 Returns a 3-tuple of (request, action, action data).
1193 1235 """
1194 1236 if not self._canissuecommands:
1195 1237 raise error.ProgrammingError('cannot issue new commands')
1196 1238
1197 1239 requestid = self._nextrequestid
1198 1240 self._nextrequestid += 2
1199 1241
1200 1242 request = commandrequest(requestid, name, args, datafh=datafh,
1201 1243 redirect=redirect)
1202 1244
1203 1245 if self._buffersends:
1204 1246 self._pendingrequests.append(request)
1205 1247 return request, 'noop', {}
1206 1248 else:
1207 1249 if not self._cansend:
1208 1250 raise error.ProgrammingError('sends cannot be performed on '
1209 1251 'this instance')
1210 1252
1211 1253 if not self._hasmultiplesend:
1212 1254 self._cansend = False
1213 1255 self._canissuecommands = False
1214 1256
1215 1257 return request, 'sendframes', {
1216 1258 'framegen': self._makecommandframes(request),
1217 1259 }
1218 1260
1219 1261 def flushcommands(self):
1220 1262 """Request that all queued commands be sent.
1221 1263
1222 1264 If any commands are buffered, this will instruct the caller to send
1223 1265 them over the wire. If no commands are buffered it instructs the client
1224 1266 to no-op.
1225 1267
1226 1268 If instances aren't configured for multiple sends, no new command
1227 1269 requests are allowed after this is called.
1228 1270 """
1229 1271 if not self._pendingrequests:
1230 1272 return 'noop', {}
1231 1273
1232 1274 if not self._cansend:
1233 1275 raise error.ProgrammingError('sends cannot be performed on this '
1234 1276 'instance')
1235 1277
1236 1278 # If the instance only allows sending once, mark that we have fired
1237 1279 # our one shot.
1238 1280 if not self._hasmultiplesend:
1239 1281 self._canissuecommands = False
1240 1282 self._cansend = False
1241 1283
1242 1284 def makeframes():
1243 1285 while self._pendingrequests:
1244 1286 request = self._pendingrequests.popleft()
1245 1287 for frame in self._makecommandframes(request):
1246 1288 yield frame
1247 1289
1248 1290 return 'sendframes', {
1249 1291 'framegen': makeframes(),
1250 1292 }
1251 1293
1252 1294 def _makecommandframes(self, request):
1253 1295 """Emit frames to issue a command request.
1254 1296
1255 1297 As a side-effect, update request accounting to reflect its changed
1256 1298 state.
1257 1299 """
1258 1300 self._activerequests[request.requestid] = request
1259 1301 request.state = 'sending'
1260 1302
1261 1303 res = createcommandframes(self._outgoingstream,
1262 1304 request.requestid,
1263 1305 request.name,
1264 1306 request.args,
1265 1307 datafh=request.datafh,
1266 1308 redirect=request.redirect)
1267 1309
1268 1310 for frame in res:
1269 1311 yield frame
1270 1312
1271 1313 request.state = 'sent'
1272 1314
1273 1315 def onframerecv(self, frame):
1274 1316 """Process a frame that has been received off the wire.
1275 1317
1276 1318 Returns a 2-tuple of (action, meta) describing further action the
1277 1319 caller needs to take as a result of receiving this frame.
1278 1320 """
1279 1321 if frame.streamid % 2:
1280 1322 return 'error', {
1281 1323 'message': (
1282 1324 _('received frame with odd numbered stream ID: %d') %
1283 1325 frame.streamid),
1284 1326 }
1285 1327
1286 1328 if frame.streamid not in self._incomingstreams:
1287 1329 if not frame.streamflags & STREAM_FLAG_BEGIN_STREAM:
1288 1330 return 'error', {
1289 1331 'message': _('received frame on unknown stream '
1290 1332 'without beginning of stream flag set'),
1291 1333 }
1292 1334
1293 1335 self._incomingstreams[frame.streamid] = stream(frame.streamid)
1294 1336
1295 1337 if frame.streamflags & STREAM_FLAG_ENCODING_APPLIED:
1296 1338 raise error.ProgrammingError('support for decoding stream '
1297 1339 'payloads not yet implemneted')
1298 1340
1299 1341 if frame.streamflags & STREAM_FLAG_END_STREAM:
1300 1342 del self._incomingstreams[frame.streamid]
1301 1343
1302 1344 if frame.requestid not in self._activerequests:
1303 1345 return 'error', {
1304 1346 'message': (_('received frame for inactive request ID: %d') %
1305 1347 frame.requestid),
1306 1348 }
1307 1349
1308 1350 request = self._activerequests[frame.requestid]
1309 1351 request.state = 'receiving'
1310 1352
1311 1353 handlers = {
1312 1354 FRAME_TYPE_COMMAND_RESPONSE: self._oncommandresponseframe,
1313 1355 FRAME_TYPE_ERROR_RESPONSE: self._onerrorresponseframe,
1314 1356 }
1315 1357
1316 1358 meth = handlers.get(frame.typeid)
1317 1359 if not meth:
1318 1360 raise error.ProgrammingError('unhandled frame type: %d' %
1319 1361 frame.typeid)
1320 1362
1321 1363 return meth(request, frame)
1322 1364
1323 1365 def _oncommandresponseframe(self, request, frame):
1324 1366 if frame.flags & FLAG_COMMAND_RESPONSE_EOS:
1325 1367 request.state = 'received'
1326 1368 del self._activerequests[request.requestid]
1327 1369
1328 1370 return 'responsedata', {
1329 1371 'request': request,
1330 1372 'expectmore': frame.flags & FLAG_COMMAND_RESPONSE_CONTINUATION,
1331 1373 'eos': frame.flags & FLAG_COMMAND_RESPONSE_EOS,
1332 1374 'data': frame.payload,
1333 1375 }
1334 1376
1335 1377 def _onerrorresponseframe(self, request, frame):
1336 1378 request.state = 'errored'
1337 1379 del self._activerequests[request.requestid]
1338 1380
1339 1381 # The payload should be a CBOR map.
1340 1382 m = cborutil.decodeall(frame.payload)[0]
1341 1383
1342 1384 return 'error', {
1343 1385 'request': request,
1344 1386 'type': m['type'],
1345 1387 'message': m['message'],
1346 1388 }
@@ -1,370 +1,387 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 8 from .node import (
9 9 bin,
10 10 hex,
11 11 )
12 12 from .i18n import _
13 13 from .thirdparty import (
14 14 attr,
15 15 )
16 16 from . import (
17 17 error,
18 18 util,
19 19 )
20 20 from .utils import (
21 21 interfaceutil,
22 22 )
23 23
24 24 # Names of the SSH protocol implementations.
25 25 SSHV1 = 'ssh-v1'
26 26 # These are advertised over the wire. Increment the counters at the end
27 27 # to reflect BC breakages.
28 28 SSHV2 = 'exp-ssh-v2-0002'
29 29 HTTP_WIREPROTO_V2 = 'exp-http-v2-0002'
30 30
31 31 # All available wire protocol transports.
32 32 TRANSPORTS = {
33 33 SSHV1: {
34 34 'transport': 'ssh',
35 35 'version': 1,
36 36 },
37 37 SSHV2: {
38 38 'transport': 'ssh',
39 39 # TODO mark as version 2 once all commands are implemented.
40 40 'version': 1,
41 41 },
42 42 'http-v1': {
43 43 'transport': 'http',
44 44 'version': 1,
45 45 },
46 46 HTTP_WIREPROTO_V2: {
47 47 'transport': 'http',
48 48 'version': 2,
49 49 }
50 50 }
51 51
52 52 class bytesresponse(object):
53 53 """A wire protocol response consisting of raw bytes."""
54 54 def __init__(self, data):
55 55 self.data = data
56 56
57 57 class ooberror(object):
58 58 """wireproto reply: failure of a batch of operation
59 59
60 60 Something failed during a batch call. The error message is stored in
61 61 `self.message`.
62 62 """
63 63 def __init__(self, message):
64 64 self.message = message
65 65
66 66 class pushres(object):
67 67 """wireproto reply: success with simple integer return
68 68
69 69 The call was successful and returned an integer contained in `self.res`.
70 70 """
71 71 def __init__(self, res, output):
72 72 self.res = res
73 73 self.output = output
74 74
75 75 class pusherr(object):
76 76 """wireproto reply: failure
77 77
78 78 The call failed. The `self.res` attribute contains the error message.
79 79 """
80 80 def __init__(self, res, output):
81 81 self.res = res
82 82 self.output = output
83 83
84 84 class streamres(object):
85 85 """wireproto reply: binary stream
86 86
87 87 The call was successful and the result is a stream.
88 88
89 89 Accepts a generator containing chunks of data to be sent to the client.
90 90
91 91 ``prefer_uncompressed`` indicates that the data is expected to be
92 92 uncompressable and that the stream should therefore use the ``none``
93 93 engine.
94 94 """
95 95 def __init__(self, gen=None, prefer_uncompressed=False):
96 96 self.gen = gen
97 97 self.prefer_uncompressed = prefer_uncompressed
98 98
99 99 class streamreslegacy(object):
100 100 """wireproto reply: uncompressed binary stream
101 101
102 102 The call was successful and the result is a stream.
103 103
104 104 Accepts a generator containing chunks of data to be sent to the client.
105 105
106 106 Like ``streamres``, but sends an uncompressed data for "version 1" clients
107 107 using the application/mercurial-0.1 media type.
108 108 """
109 109 def __init__(self, gen=None):
110 110 self.gen = gen
111 111
112 112 # list of nodes encoding / decoding
113 113 def decodelist(l, sep=' '):
114 114 if l:
115 115 return [bin(v) for v in l.split(sep)]
116 116 return []
117 117
118 118 def encodelist(l, sep=' '):
119 119 try:
120 120 return sep.join(map(hex, l))
121 121 except TypeError:
122 122 raise
123 123
124 124 # batched call argument encoding
125 125
126 126 def escapebatcharg(plain):
127 127 return (plain
128 128 .replace(':', ':c')
129 129 .replace(',', ':o')
130 130 .replace(';', ':s')
131 131 .replace('=', ':e'))
132 132
133 133 def unescapebatcharg(escaped):
134 134 return (escaped
135 135 .replace(':e', '=')
136 136 .replace(':s', ';')
137 137 .replace(':o', ',')
138 138 .replace(':c', ':'))
139 139
140 140 # mapping of options accepted by getbundle and their types
141 141 #
142 142 # Meant to be extended by extensions. It is extensions responsibility to ensure
143 143 # such options are properly processed in exchange.getbundle.
144 144 #
145 145 # supported types are:
146 146 #
147 147 # :nodes: list of binary nodes
148 148 # :csv: list of comma-separated values
149 149 # :scsv: list of comma-separated values return as set
150 150 # :plain: string with no transformation needed.
151 151 GETBUNDLE_ARGUMENTS = {
152 152 'heads': 'nodes',
153 153 'bookmarks': 'boolean',
154 154 'common': 'nodes',
155 155 'obsmarkers': 'boolean',
156 156 'phases': 'boolean',
157 157 'bundlecaps': 'scsv',
158 158 'listkeys': 'csv',
159 159 'cg': 'boolean',
160 160 'cbattempted': 'boolean',
161 161 'stream': 'boolean',
162 162 }
163 163
164 164 class baseprotocolhandler(interfaceutil.Interface):
165 165 """Abstract base class for wire protocol handlers.
166 166
167 167 A wire protocol handler serves as an interface between protocol command
168 168 handlers and the wire protocol transport layer. Protocol handlers provide
169 169 methods to read command arguments, redirect stdio for the duration of
170 170 the request, handle response types, etc.
171 171 """
172 172
173 173 name = interfaceutil.Attribute(
174 174 """The name of the protocol implementation.
175 175
176 176 Used for uniquely identifying the transport type.
177 177 """)
178 178
179 179 def getargs(args):
180 180 """return the value for arguments in <args>
181 181
182 182 For version 1 transports, returns a list of values in the same
183 183 order they appear in ``args``. For version 2 transports, returns
184 184 a dict mapping argument name to value.
185 185 """
186 186
187 187 def getprotocaps():
188 188 """Returns the list of protocol-level capabilities of client
189 189
190 190 Returns a list of capabilities as declared by the client for
191 191 the current request (or connection for stateful protocol handlers)."""
192 192
193 193 def getpayload():
194 194 """Provide a generator for the raw payload.
195 195
196 196 The caller is responsible for ensuring that the full payload is
197 197 processed.
198 198 """
199 199
200 200 def mayberedirectstdio():
201 201 """Context manager to possibly redirect stdio.
202 202
203 203 The context manager yields a file-object like object that receives
204 204 stdout and stderr output when the context manager is active. Or it
205 205 yields ``None`` if no I/O redirection occurs.
206 206
207 207 The intent of this context manager is to capture stdio output
208 208 so it may be sent in the response. Some transports support streaming
209 209 stdio to the client in real time. For these transports, stdio output
210 210 won't be captured.
211 211 """
212 212
213 213 def client():
214 214 """Returns a string representation of this client (as bytes)."""
215 215
216 216 def addcapabilities(repo, caps):
217 217 """Adds advertised capabilities specific to this protocol.
218 218
219 219 Receives the list of capabilities collected so far.
220 220
221 221 Returns a list of capabilities. The passed in argument can be returned.
222 222 """
223 223
224 224 def checkperm(perm):
225 225 """Validate that the client has permissions to perform a request.
226 226
227 227 The argument is the permission required to proceed. If the client
228 228 doesn't have that permission, the exception should raise or abort
229 229 in a protocol specific manner.
230 230 """
231 231
232 232 class commandentry(object):
233 233 """Represents a declared wire protocol command."""
234 234 def __init__(self, func, args='', transports=None,
235 235 permission='push', cachekeyfn=None):
236 236 self.func = func
237 237 self.args = args
238 238 self.transports = transports or set()
239 239 self.permission = permission
240 240 self.cachekeyfn = cachekeyfn
241 241
242 242 def _merge(self, func, args):
243 243 """Merge this instance with an incoming 2-tuple.
244 244
245 245 This is called when a caller using the old 2-tuple API attempts
246 246 to replace an instance. The incoming values are merged with
247 247 data not captured by the 2-tuple and a new instance containing
248 248 the union of the two objects is returned.
249 249 """
250 250 return commandentry(func, args=args, transports=set(self.transports),
251 251 permission=self.permission)
252 252
253 253 # Old code treats instances as 2-tuples. So expose that interface.
254 254 def __iter__(self):
255 255 yield self.func
256 256 yield self.args
257 257
258 258 def __getitem__(self, i):
259 259 if i == 0:
260 260 return self.func
261 261 elif i == 1:
262 262 return self.args
263 263 else:
264 264 raise IndexError('can only access elements 0 and 1')
265 265
266 266 class commanddict(dict):
267 267 """Container for registered wire protocol commands.
268 268
269 269 It behaves like a dict. But __setitem__ is overwritten to allow silent
270 270 coercion of values from 2-tuples for API compatibility.
271 271 """
272 272 def __setitem__(self, k, v):
273 273 if isinstance(v, commandentry):
274 274 pass
275 275 # Cast 2-tuples to commandentry instances.
276 276 elif isinstance(v, tuple):
277 277 if len(v) != 2:
278 278 raise ValueError('command tuples must have exactly 2 elements')
279 279
280 280 # It is common for extensions to wrap wire protocol commands via
281 281 # e.g. ``wireproto.commands[x] = (newfn, args)``. Because callers
282 282 # doing this aren't aware of the new API that uses objects to store
283 283 # command entries, we automatically merge old state with new.
284 284 if k in self:
285 285 v = self[k]._merge(v[0], v[1])
286 286 else:
287 287 # Use default values from @wireprotocommand.
288 288 v = commandentry(v[0], args=v[1],
289 289 transports=set(TRANSPORTS),
290 290 permission='push')
291 291 else:
292 292 raise ValueError('command entries must be commandentry instances '
293 293 'or 2-tuples')
294 294
295 295 return super(commanddict, self).__setitem__(k, v)
296 296
297 297 def commandavailable(self, command, proto):
298 298 """Determine if a command is available for the requested protocol."""
299 299 assert proto.name in TRANSPORTS
300 300
301 301 entry = self.get(command)
302 302
303 303 if not entry:
304 304 return False
305 305
306 306 if proto.name not in entry.transports:
307 307 return False
308 308
309 309 return True
310 310
311 311 def supportedcompengines(ui, role):
312 312 """Obtain the list of supported compression engines for a request."""
313 313 assert role in (util.CLIENTROLE, util.SERVERROLE)
314 314
315 315 compengines = util.compengines.supportedwireengines(role)
316 316
317 317 # Allow config to override default list and ordering.
318 318 if role == util.SERVERROLE:
319 319 configengines = ui.configlist('server', 'compressionengines')
320 320 config = 'server.compressionengines'
321 321 else:
322 322 # This is currently implemented mainly to facilitate testing. In most
323 323 # cases, the server should be in charge of choosing a compression engine
324 324 # because a server has the most to lose from a sub-optimal choice. (e.g.
325 325 # CPU DoS due to an expensive engine or a network DoS due to poor
326 326 # compression ratio).
327 327 configengines = ui.configlist('experimental',
328 328 'clientcompressionengines')
329 329 config = 'experimental.clientcompressionengines'
330 330
331 331 # No explicit config. Filter out the ones that aren't supposed to be
332 332 # advertised and return default ordering.
333 333 if not configengines:
334 334 attr = 'serverpriority' if role == util.SERVERROLE else 'clientpriority'
335 335 return [e for e in compengines
336 336 if getattr(e.wireprotosupport(), attr) > 0]
337 337
338 338 # If compression engines are listed in the config, assume there is a good
339 339 # reason for it (like server operators wanting to achieve specific
340 340 # performance characteristics). So fail fast if the config references
341 341 # unusable compression engines.
342 342 validnames = set(e.name() for e in compengines)
343 343 invalidnames = set(e for e in configengines if e not in validnames)
344 344 if invalidnames:
345 345 raise error.Abort(_('invalid compression engine defined in %s: %s') %
346 346 (config, ', '.join(sorted(invalidnames))))
347 347
348 348 compengines = [e for e in compengines if e.name() in configengines]
349 349 compengines = sorted(compengines,
350 350 key=lambda e: configengines.index(e.name()))
351 351
352 352 if not compengines:
353 353 raise error.Abort(_('%s config option does not specify any known '
354 354 'compression engines') % config,
355 355 hint=_('usable compression engines: %s') %
356 356 ', '.sorted(validnames))
357 357
358 358 return compengines
359 359
360 360 @attr.s
361 361 class encodedresponse(object):
362 362 """Represents response data that is already content encoded.
363 363
364 364 Wire protocol version 2 only.
365 365
366 366 Commands typically emit Python objects that are encoded and sent over the
367 367 wire. If commands emit an object of this type, the encoding step is bypassed
368 368 and the content from this object is used instead.
369 369 """
370 370 data = attr.ib()
371
372 @attr.s
373 class alternatelocationresponse(object):
374 """Represents a response available at an alternate location.
375
376 Instances are sent in place of actual response objects when the server
377 is sending a "content redirect" response.
378
379 Only compatible with wire protocol version 2.
380 """
381 url = attr.ib()
382 mediatype = attr.ib()
383 size = attr.ib(default=None)
384 fullhashes = attr.ib(default=None)
385 fullhashseed = attr.ib(default=None)
386 serverdercerts = attr.ib(default=None)
387 servercadercerts = attr.ib(default=None)
@@ -1,1187 +1,1197 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 9 import contextlib
10 10 import hashlib
11 11
12 12 from .i18n import _
13 13 from .node import (
14 14 hex,
15 15 nullid,
16 16 )
17 17 from . import (
18 18 discovery,
19 19 encoding,
20 20 error,
21 21 narrowspec,
22 22 pycompat,
23 23 streamclone,
24 24 util,
25 25 wireprotoframing,
26 26 wireprototypes,
27 27 )
28 28 from .utils import (
29 29 cborutil,
30 30 interfaceutil,
31 31 stringutil,
32 32 )
33 33
34 34 FRAMINGTYPE = b'application/mercurial-exp-framing-0005'
35 35
36 36 HTTP_WIREPROTO_V2 = wireprototypes.HTTP_WIREPROTO_V2
37 37
38 38 COMMANDS = wireprototypes.commanddict()
39 39
40 40 # Value inserted into cache key computation function. Change the value to
41 41 # force new cache keys for every command request. This should be done when
42 42 # there is a change to how caching works, etc.
43 43 GLOBAL_CACHE_VERSION = 1
44 44
45 45 def handlehttpv2request(rctx, req, res, checkperm, urlparts):
46 46 from .hgweb import common as hgwebcommon
47 47
48 48 # URL space looks like: <permissions>/<command>, where <permission> can
49 49 # be ``ro`` or ``rw`` to signal read-only or read-write, respectively.
50 50
51 51 # Root URL does nothing meaningful... yet.
52 52 if not urlparts:
53 53 res.status = b'200 OK'
54 54 res.headers[b'Content-Type'] = b'text/plain'
55 55 res.setbodybytes(_('HTTP version 2 API handler'))
56 56 return
57 57
58 58 if len(urlparts) == 1:
59 59 res.status = b'404 Not Found'
60 60 res.headers[b'Content-Type'] = b'text/plain'
61 61 res.setbodybytes(_('do not know how to process %s\n') %
62 62 req.dispatchpath)
63 63 return
64 64
65 65 permission, command = urlparts[0:2]
66 66
67 67 if permission not in (b'ro', b'rw'):
68 68 res.status = b'404 Not Found'
69 69 res.headers[b'Content-Type'] = b'text/plain'
70 70 res.setbodybytes(_('unknown permission: %s') % permission)
71 71 return
72 72
73 73 if req.method != 'POST':
74 74 res.status = b'405 Method Not Allowed'
75 75 res.headers[b'Allow'] = b'POST'
76 76 res.setbodybytes(_('commands require POST requests'))
77 77 return
78 78
79 79 # At some point we'll want to use our own API instead of recycling the
80 80 # behavior of version 1 of the wire protocol...
81 81 # TODO return reasonable responses - not responses that overload the
82 82 # HTTP status line message for error reporting.
83 83 try:
84 84 checkperm(rctx, req, 'pull' if permission == b'ro' else 'push')
85 85 except hgwebcommon.ErrorResponse as e:
86 86 res.status = hgwebcommon.statusmessage(e.code, pycompat.bytestr(e))
87 87 for k, v in e.headers:
88 88 res.headers[k] = v
89 89 res.setbodybytes('permission denied')
90 90 return
91 91
92 92 # We have a special endpoint to reflect the request back at the client.
93 93 if command == b'debugreflect':
94 94 _processhttpv2reflectrequest(rctx.repo.ui, rctx.repo, req, res)
95 95 return
96 96
97 97 # Extra commands that we handle that aren't really wire protocol
98 98 # commands. Think extra hard before making this hackery available to
99 99 # extension.
100 100 extracommands = {'multirequest'}
101 101
102 102 if command not in COMMANDS and command not in extracommands:
103 103 res.status = b'404 Not Found'
104 104 res.headers[b'Content-Type'] = b'text/plain'
105 105 res.setbodybytes(_('unknown wire protocol command: %s\n') % command)
106 106 return
107 107
108 108 repo = rctx.repo
109 109 ui = repo.ui
110 110
111 111 proto = httpv2protocolhandler(req, ui)
112 112
113 113 if (not COMMANDS.commandavailable(command, proto)
114 114 and command not in extracommands):
115 115 res.status = b'404 Not Found'
116 116 res.headers[b'Content-Type'] = b'text/plain'
117 117 res.setbodybytes(_('invalid wire protocol command: %s') % command)
118 118 return
119 119
120 120 # TODO consider cases where proxies may add additional Accept headers.
121 121 if req.headers.get(b'Accept') != FRAMINGTYPE:
122 122 res.status = b'406 Not Acceptable'
123 123 res.headers[b'Content-Type'] = b'text/plain'
124 124 res.setbodybytes(_('client MUST specify Accept header with value: %s\n')
125 125 % FRAMINGTYPE)
126 126 return
127 127
128 128 if req.headers.get(b'Content-Type') != FRAMINGTYPE:
129 129 res.status = b'415 Unsupported Media Type'
130 130 # TODO we should send a response with appropriate media type,
131 131 # since client does Accept it.
132 132 res.headers[b'Content-Type'] = b'text/plain'
133 133 res.setbodybytes(_('client MUST send Content-Type header with '
134 134 'value: %s\n') % FRAMINGTYPE)
135 135 return
136 136
137 137 _processhttpv2request(ui, repo, req, res, permission, command, proto)
138 138
139 139 def _processhttpv2reflectrequest(ui, repo, req, res):
140 140 """Reads unified frame protocol request and dumps out state to client.
141 141
142 142 This special endpoint can be used to help debug the wire protocol.
143 143
144 144 Instead of routing the request through the normal dispatch mechanism,
145 145 we instead read all frames, decode them, and feed them into our state
146 146 tracker. We then dump the log of all that activity back out to the
147 147 client.
148 148 """
149 149 import json
150 150
151 151 # Reflection APIs have a history of being abused, accidentally disclosing
152 152 # sensitive data, etc. So we have a config knob.
153 153 if not ui.configbool('experimental', 'web.api.debugreflect'):
154 154 res.status = b'404 Not Found'
155 155 res.headers[b'Content-Type'] = b'text/plain'
156 156 res.setbodybytes(_('debugreflect service not available'))
157 157 return
158 158
159 159 # We assume we have a unified framing protocol request body.
160 160
161 161 reactor = wireprotoframing.serverreactor()
162 162 states = []
163 163
164 164 while True:
165 165 frame = wireprotoframing.readframe(req.bodyfh)
166 166
167 167 if not frame:
168 168 states.append(b'received: <no frame>')
169 169 break
170 170
171 171 states.append(b'received: %d %d %d %s' % (frame.typeid, frame.flags,
172 172 frame.requestid,
173 173 frame.payload))
174 174
175 175 action, meta = reactor.onframerecv(frame)
176 176 states.append(json.dumps((action, meta), sort_keys=True,
177 177 separators=(', ', ': ')))
178 178
179 179 action, meta = reactor.oninputeof()
180 180 meta['action'] = action
181 181 states.append(json.dumps(meta, sort_keys=True, separators=(', ',': ')))
182 182
183 183 res.status = b'200 OK'
184 184 res.headers[b'Content-Type'] = b'text/plain'
185 185 res.setbodybytes(b'\n'.join(states))
186 186
187 187 def _processhttpv2request(ui, repo, req, res, authedperm, reqcommand, proto):
188 188 """Post-validation handler for HTTPv2 requests.
189 189
190 190 Called when the HTTP request contains unified frame-based protocol
191 191 frames for evaluation.
192 192 """
193 193 # TODO Some HTTP clients are full duplex and can receive data before
194 194 # the entire request is transmitted. Figure out a way to indicate support
195 195 # for that so we can opt into full duplex mode.
196 196 reactor = wireprotoframing.serverreactor(deferoutput=True)
197 197 seencommand = False
198 198
199 199 outstream = reactor.makeoutputstream()
200 200
201 201 while True:
202 202 frame = wireprotoframing.readframe(req.bodyfh)
203 203 if not frame:
204 204 break
205 205
206 206 action, meta = reactor.onframerecv(frame)
207 207
208 208 if action == 'wantframe':
209 209 # Need more data before we can do anything.
210 210 continue
211 211 elif action == 'runcommand':
212 212 sentoutput = _httpv2runcommand(ui, repo, req, res, authedperm,
213 213 reqcommand, reactor, outstream,
214 214 meta, issubsequent=seencommand)
215 215
216 216 if sentoutput:
217 217 return
218 218
219 219 seencommand = True
220 220
221 221 elif action == 'error':
222 222 # TODO define proper error mechanism.
223 223 res.status = b'200 OK'
224 224 res.headers[b'Content-Type'] = b'text/plain'
225 225 res.setbodybytes(meta['message'] + b'\n')
226 226 return
227 227 else:
228 228 raise error.ProgrammingError(
229 229 'unhandled action from frame processor: %s' % action)
230 230
231 231 action, meta = reactor.oninputeof()
232 232 if action == 'sendframes':
233 233 # We assume we haven't started sending the response yet. If we're
234 234 # wrong, the response type will raise an exception.
235 235 res.status = b'200 OK'
236 236 res.headers[b'Content-Type'] = FRAMINGTYPE
237 237 res.setbodygen(meta['framegen'])
238 238 elif action == 'noop':
239 239 pass
240 240 else:
241 241 raise error.ProgrammingError('unhandled action from frame processor: %s'
242 242 % action)
243 243
244 244 def _httpv2runcommand(ui, repo, req, res, authedperm, reqcommand, reactor,
245 245 outstream, command, issubsequent):
246 246 """Dispatch a wire protocol command made from HTTPv2 requests.
247 247
248 248 The authenticated permission (``authedperm``) along with the original
249 249 command from the URL (``reqcommand``) are passed in.
250 250 """
251 251 # We already validated that the session has permissions to perform the
252 252 # actions in ``authedperm``. In the unified frame protocol, the canonical
253 253 # command to run is expressed in a frame. However, the URL also requested
254 254 # to run a specific command. We need to be careful that the command we
255 255 # run doesn't have permissions requirements greater than what was granted
256 256 # by ``authedperm``.
257 257 #
258 258 # Our rule for this is we only allow one command per HTTP request and
259 259 # that command must match the command in the URL. However, we make
260 260 # an exception for the ``multirequest`` URL. This URL is allowed to
261 261 # execute multiple commands. We double check permissions of each command
262 262 # as it is invoked to ensure there is no privilege escalation.
263 263 # TODO consider allowing multiple commands to regular command URLs
264 264 # iff each command is the same.
265 265
266 266 proto = httpv2protocolhandler(req, ui, args=command['args'])
267 267
268 268 if reqcommand == b'multirequest':
269 269 if not COMMANDS.commandavailable(command['command'], proto):
270 270 # TODO proper error mechanism
271 271 res.status = b'200 OK'
272 272 res.headers[b'Content-Type'] = b'text/plain'
273 273 res.setbodybytes(_('wire protocol command not available: %s') %
274 274 command['command'])
275 275 return True
276 276
277 277 # TODO don't use assert here, since it may be elided by -O.
278 278 assert authedperm in (b'ro', b'rw')
279 279 wirecommand = COMMANDS[command['command']]
280 280 assert wirecommand.permission in ('push', 'pull')
281 281
282 282 if authedperm == b'ro' and wirecommand.permission != 'pull':
283 283 # TODO proper error mechanism
284 284 res.status = b'403 Forbidden'
285 285 res.headers[b'Content-Type'] = b'text/plain'
286 286 res.setbodybytes(_('insufficient permissions to execute '
287 287 'command: %s') % command['command'])
288 288 return True
289 289
290 290 # TODO should we also call checkperm() here? Maybe not if we're going
291 291 # to overhaul that API. The granted scope from the URL check should
292 292 # be good enough.
293 293
294 294 else:
295 295 # Don't allow multiple commands outside of ``multirequest`` URL.
296 296 if issubsequent:
297 297 # TODO proper error mechanism
298 298 res.status = b'200 OK'
299 299 res.headers[b'Content-Type'] = b'text/plain'
300 300 res.setbodybytes(_('multiple commands cannot be issued to this '
301 301 'URL'))
302 302 return True
303 303
304 304 if reqcommand != command['command']:
305 305 # TODO define proper error mechanism
306 306 res.status = b'200 OK'
307 307 res.headers[b'Content-Type'] = b'text/plain'
308 308 res.setbodybytes(_('command in frame must match command in URL'))
309 309 return True
310 310
311 311 res.status = b'200 OK'
312 312 res.headers[b'Content-Type'] = FRAMINGTYPE
313 313
314 314 try:
315 objs = dispatch(repo, proto, command['command'])
315 objs = dispatch(repo, proto, command['command'], command['redirect'])
316 316
317 317 action, meta = reactor.oncommandresponsereadyobjects(
318 318 outstream, command['requestid'], objs)
319 319
320 320 except error.WireprotoCommandError as e:
321 321 action, meta = reactor.oncommanderror(
322 322 outstream, command['requestid'], e.message, e.messageargs)
323 323
324 324 except Exception as e:
325 325 action, meta = reactor.onservererror(
326 326 outstream, command['requestid'],
327 327 _('exception when invoking command: %s') %
328 328 stringutil.forcebytestr(e))
329 329
330 330 if action == 'sendframes':
331 331 res.setbodygen(meta['framegen'])
332 332 return True
333 333 elif action == 'noop':
334 334 return False
335 335 else:
336 336 raise error.ProgrammingError('unhandled event from reactor: %s' %
337 337 action)
338 338
339 339 def getdispatchrepo(repo, proto, command):
340 340 return repo.filtered('served')
341 341
342 def dispatch(repo, proto, command):
342 def dispatch(repo, proto, command, redirect):
343 343 """Run a wire protocol command.
344 344
345 345 Returns an iterable of objects that will be sent to the client.
346 346 """
347 347 repo = getdispatchrepo(repo, proto, command)
348 348
349 349 entry = COMMANDS[command]
350 350 func = entry.func
351 351 spec = entry.args
352 352
353 353 args = proto.getargs(spec)
354 354
355 355 # There is some duplicate boilerplate code here for calling the command and
356 356 # emitting objects. It is either that or a lot of indented code that looks
357 357 # like a pyramid (since there are a lot of code paths that result in not
358 358 # using the cacher).
359 359 callcommand = lambda: func(repo, proto, **pycompat.strkwargs(args))
360 360
361 361 # Request is not cacheable. Don't bother instantiating a cacher.
362 362 if not entry.cachekeyfn:
363 363 for o in callcommand():
364 364 yield o
365 365 return
366 366
367 if redirect:
368 redirecttargets = redirect[b'targets']
369 redirecthashes = redirect[b'hashes']
370 else:
371 redirecttargets = []
372 redirecthashes = []
373
367 374 cacher = makeresponsecacher(repo, proto, command, args,
368 cborutil.streamencode)
375 cborutil.streamencode,
376 redirecttargets=redirecttargets,
377 redirecthashes=redirecthashes)
369 378
370 379 # But we have no cacher. Do default handling.
371 380 if not cacher:
372 381 for o in callcommand():
373 382 yield o
374 383 return
375 384
376 385 with cacher:
377 386 cachekey = entry.cachekeyfn(repo, proto, cacher, **args)
378 387
379 388 # No cache key or the cacher doesn't like it. Do default handling.
380 389 if cachekey is None or not cacher.setcachekey(cachekey):
381 390 for o in callcommand():
382 391 yield o
383 392 return
384 393
385 394 # Serve it from the cache, if possible.
386 395 cached = cacher.lookup()
387 396
388 397 if cached:
389 398 for o in cached['objs']:
390 399 yield o
391 400 return
392 401
393 402 # Else call the command and feed its output into the cacher, allowing
394 403 # the cacher to buffer/mutate objects as it desires.
395 404 for o in callcommand():
396 405 for o in cacher.onobject(o):
397 406 yield o
398 407
399 408 for o in cacher.onfinished():
400 409 yield o
401 410
402 411 @interfaceutil.implementer(wireprototypes.baseprotocolhandler)
403 412 class httpv2protocolhandler(object):
404 413 def __init__(self, req, ui, args=None):
405 414 self._req = req
406 415 self._ui = ui
407 416 self._args = args
408 417
409 418 @property
410 419 def name(self):
411 420 return HTTP_WIREPROTO_V2
412 421
413 422 def getargs(self, args):
414 423 # First look for args that were passed but aren't registered on this
415 424 # command.
416 425 extra = set(self._args) - set(args)
417 426 if extra:
418 427 raise error.WireprotoCommandError(
419 428 'unsupported argument to command: %s' %
420 429 ', '.join(sorted(extra)))
421 430
422 431 # And look for required arguments that are missing.
423 432 missing = {a for a in args if args[a]['required']} - set(self._args)
424 433
425 434 if missing:
426 435 raise error.WireprotoCommandError(
427 436 'missing required arguments: %s' % ', '.join(sorted(missing)))
428 437
429 438 # Now derive the arguments to pass to the command, taking into
430 439 # account the arguments specified by the client.
431 440 data = {}
432 441 for k, meta in sorted(args.items()):
433 442 # This argument wasn't passed by the client.
434 443 if k not in self._args:
435 444 data[k] = meta['default']()
436 445 continue
437 446
438 447 v = self._args[k]
439 448
440 449 # Sets may be expressed as lists. Silently normalize.
441 450 if meta['type'] == 'set' and isinstance(v, list):
442 451 v = set(v)
443 452
444 453 # TODO consider more/stronger type validation.
445 454
446 455 data[k] = v
447 456
448 457 return data
449 458
450 459 def getprotocaps(self):
451 460 # Protocol capabilities are currently not implemented for HTTP V2.
452 461 return set()
453 462
454 463 def getpayload(self):
455 464 raise NotImplementedError
456 465
457 466 @contextlib.contextmanager
458 467 def mayberedirectstdio(self):
459 468 raise NotImplementedError
460 469
461 470 def client(self):
462 471 raise NotImplementedError
463 472
464 473 def addcapabilities(self, repo, caps):
465 474 return caps
466 475
467 476 def checkperm(self, perm):
468 477 raise NotImplementedError
469 478
470 479 def httpv2apidescriptor(req, repo):
471 480 proto = httpv2protocolhandler(req, repo.ui)
472 481
473 482 return _capabilitiesv2(repo, proto)
474 483
475 484 def _capabilitiesv2(repo, proto):
476 485 """Obtain the set of capabilities for version 2 transports.
477 486
478 487 These capabilities are distinct from the capabilities for version 1
479 488 transports.
480 489 """
481 490 compression = []
482 491 for engine in wireprototypes.supportedcompengines(repo.ui, util.SERVERROLE):
483 492 compression.append({
484 493 b'name': engine.wireprotosupport().name,
485 494 })
486 495
487 496 caps = {
488 497 'commands': {},
489 498 'compression': compression,
490 499 'framingmediatypes': [FRAMINGTYPE],
491 500 'pathfilterprefixes': set(narrowspec.VALID_PREFIXES),
492 501 }
493 502
494 503 for command, entry in COMMANDS.items():
495 504 args = {}
496 505
497 506 for arg, meta in entry.args.items():
498 507 args[arg] = {
499 508 # TODO should this be a normalized type using CBOR's
500 509 # terminology?
501 510 b'type': meta['type'],
502 511 b'required': meta['required'],
503 512 }
504 513
505 514 if not meta['required']:
506 515 args[arg][b'default'] = meta['default']()
507 516
508 517 if meta['validvalues']:
509 518 args[arg][b'validvalues'] = meta['validvalues']
510 519
511 520 caps['commands'][command] = {
512 521 'args': args,
513 522 'permissions': [entry.permission],
514 523 }
515 524
516 525 if streamclone.allowservergeneration(repo):
517 526 caps['rawrepoformats'] = sorted(repo.requirements &
518 527 repo.supportedformats)
519 528
520 529 targets = getadvertisedredirecttargets(repo, proto)
521 530 if targets:
522 531 caps[b'redirect'] = {
523 532 b'targets': [],
524 533 b'hashes': [b'sha256', b'sha1'],
525 534 }
526 535
527 536 for target in targets:
528 537 entry = {
529 538 b'name': target['name'],
530 539 b'protocol': target['protocol'],
531 540 b'uris': target['uris'],
532 541 }
533 542
534 543 for key in ('snirequired', 'tlsversions'):
535 544 if key in target:
536 545 entry[key] = target[key]
537 546
538 547 caps[b'redirect'][b'targets'].append(entry)
539 548
540 549 return proto.addcapabilities(repo, caps)
541 550
542 551 def getadvertisedredirecttargets(repo, proto):
543 552 """Obtain a list of content redirect targets.
544 553
545 554 Returns a list containing potential redirect targets that will be
546 555 advertised in capabilities data. Each dict MUST have the following
547 556 keys:
548 557
549 558 name
550 559 The name of this redirect target. This is the identifier clients use
551 560 to refer to a target. It is transferred as part of every command
552 561 request.
553 562
554 563 protocol
555 564 Network protocol used by this target. Typically this is the string
556 565 in front of the ``://`` in a URL. e.g. ``https``.
557 566
558 567 uris
559 568 List of representative URIs for this target. Clients can use the
560 569 URIs to test parsing for compatibility or for ordering preference
561 570 for which target to use.
562 571
563 572 The following optional keys are recognized:
564 573
565 574 snirequired
566 575 Bool indicating if Server Name Indication (SNI) is required to
567 576 connect to this target.
568 577
569 578 tlsversions
570 579 List of bytes indicating which TLS versions are supported by this
571 580 target.
572 581
573 582 By default, clients reflect the target order advertised by servers
574 583 and servers will use the first client-advertised target when picking
575 584 a redirect target. So targets should be advertised in the order the
576 585 server prefers they be used.
577 586 """
578 587 return []
579 588
580 589 def wireprotocommand(name, args=None, permission='push', cachekeyfn=None):
581 590 """Decorator to declare a wire protocol command.
582 591
583 592 ``name`` is the name of the wire protocol command being provided.
584 593
585 594 ``args`` is a dict defining arguments accepted by the command. Keys are
586 595 the argument name. Values are dicts with the following keys:
587 596
588 597 ``type``
589 598 The argument data type. Must be one of the following string
590 599 literals: ``bytes``, ``int``, ``list``, ``dict``, ``set``,
591 600 or ``bool``.
592 601
593 602 ``default``
594 603 A callable returning the default value for this argument. If not
595 604 specified, ``None`` will be the default value.
596 605
597 606 ``example``
598 607 An example value for this argument.
599 608
600 609 ``validvalues``
601 610 Set of recognized values for this argument.
602 611
603 612 ``permission`` defines the permission type needed to run this command.
604 613 Can be ``push`` or ``pull``. These roughly map to read-write and read-only,
605 614 respectively. Default is to assume command requires ``push`` permissions
606 615 because otherwise commands not declaring their permissions could modify
607 616 a repository that is supposed to be read-only.
608 617
609 618 ``cachekeyfn`` defines an optional callable that can derive the
610 619 cache key for this request.
611 620
612 621 Wire protocol commands are generators of objects to be serialized and
613 622 sent to the client.
614 623
615 624 If a command raises an uncaught exception, this will be translated into
616 625 a command error.
617 626
618 627 All commands can opt in to being cacheable by defining a function
619 628 (``cachekeyfn``) that is called to derive a cache key. This function
620 629 receives the same arguments as the command itself plus a ``cacher``
621 630 argument containing the active cacher for the request and returns a bytes
622 631 containing the key in a cache the response to this command may be cached
623 632 under.
624 633 """
625 634 transports = {k for k, v in wireprototypes.TRANSPORTS.items()
626 635 if v['version'] == 2}
627 636
628 637 if permission not in ('push', 'pull'):
629 638 raise error.ProgrammingError('invalid wire protocol permission; '
630 639 'got %s; expected "push" or "pull"' %
631 640 permission)
632 641
633 642 if args is None:
634 643 args = {}
635 644
636 645 if not isinstance(args, dict):
637 646 raise error.ProgrammingError('arguments for version 2 commands '
638 647 'must be declared as dicts')
639 648
640 649 for arg, meta in args.items():
641 650 if arg == '*':
642 651 raise error.ProgrammingError('* argument name not allowed on '
643 652 'version 2 commands')
644 653
645 654 if not isinstance(meta, dict):
646 655 raise error.ProgrammingError('arguments for version 2 commands '
647 656 'must declare metadata as a dict')
648 657
649 658 if 'type' not in meta:
650 659 raise error.ProgrammingError('%s argument for command %s does not '
651 660 'declare type field' % (arg, name))
652 661
653 662 if meta['type'] not in ('bytes', 'int', 'list', 'dict', 'set', 'bool'):
654 663 raise error.ProgrammingError('%s argument for command %s has '
655 664 'illegal type: %s' % (arg, name,
656 665 meta['type']))
657 666
658 667 if 'example' not in meta:
659 668 raise error.ProgrammingError('%s argument for command %s does not '
660 669 'declare example field' % (arg, name))
661 670
662 671 meta['required'] = 'default' not in meta
663 672
664 673 meta.setdefault('default', lambda: None)
665 674 meta.setdefault('validvalues', None)
666 675
667 676 def register(func):
668 677 if name in COMMANDS:
669 678 raise error.ProgrammingError('%s command already registered '
670 679 'for version 2' % name)
671 680
672 681 COMMANDS[name] = wireprototypes.commandentry(
673 682 func, args=args, transports=transports, permission=permission,
674 683 cachekeyfn=cachekeyfn)
675 684
676 685 return func
677 686
678 687 return register
679 688
680 689 def makecommandcachekeyfn(command, localversion=None, allargs=False):
681 690 """Construct a cache key derivation function with common features.
682 691
683 692 By default, the cache key is a hash of:
684 693
685 694 * The command name.
686 695 * A global cache version number.
687 696 * A local cache version number (passed via ``localversion``).
688 697 * All the arguments passed to the command.
689 698 * The media type used.
690 699 * Wire protocol version string.
691 700 * The repository path.
692 701 """
693 702 if not allargs:
694 703 raise error.ProgrammingError('only allargs=True is currently supported')
695 704
696 705 if localversion is None:
697 706 raise error.ProgrammingError('must set localversion argument value')
698 707
699 708 def cachekeyfn(repo, proto, cacher, **args):
700 709 spec = COMMANDS[command]
701 710
702 711 # Commands that mutate the repo can not be cached.
703 712 if spec.permission == 'push':
704 713 return None
705 714
706 715 # TODO config option to disable caching.
707 716
708 717 # Our key derivation strategy is to construct a data structure
709 718 # holding everything that could influence cacheability and to hash
710 719 # the CBOR representation of that. Using CBOR seems like it might
711 720 # be overkill. However, simpler hashing mechanisms are prone to
712 721 # duplicate input issues. e.g. if you just concatenate two values,
713 722 # "foo"+"bar" is identical to "fo"+"obar". Using CBOR provides
714 723 # "padding" between values and prevents these problems.
715 724
716 725 # Seed the hash with various data.
717 726 state = {
718 727 # To invalidate all cache keys.
719 728 b'globalversion': GLOBAL_CACHE_VERSION,
720 729 # More granular cache key invalidation.
721 730 b'localversion': localversion,
722 731 # Cache keys are segmented by command.
723 732 b'command': pycompat.sysbytes(command),
724 733 # Throw in the media type and API version strings so changes
725 734 # to exchange semantics invalid cache.
726 735 b'mediatype': FRAMINGTYPE,
727 736 b'version': HTTP_WIREPROTO_V2,
728 737 # So same requests for different repos don't share cache keys.
729 738 b'repo': repo.root,
730 739 }
731 740
732 741 # The arguments passed to us will have already been normalized.
733 742 # Default values will be set, etc. This is important because it
734 743 # means that it doesn't matter if clients send an explicit argument
735 744 # or rely on the default value: it will all normalize to the same
736 745 # set of arguments on the server and therefore the same cache key.
737 746 #
738 747 # Arguments by their very nature must support being encoded to CBOR.
739 748 # And the CBOR encoder is deterministic. So we hash the arguments
740 749 # by feeding the CBOR of their representation into the hasher.
741 750 if allargs:
742 751 state[b'args'] = pycompat.byteskwargs(args)
743 752
744 753 cacher.adjustcachekeystate(state)
745 754
746 755 hasher = hashlib.sha1()
747 756 for chunk in cborutil.streamencode(state):
748 757 hasher.update(chunk)
749 758
750 759 return pycompat.sysbytes(hasher.hexdigest())
751 760
752 761 return cachekeyfn
753 762
754 def makeresponsecacher(repo, proto, command, args, objencoderfn):
763 def makeresponsecacher(repo, proto, command, args, objencoderfn,
764 redirecttargets, redirecthashes):
755 765 """Construct a cacher for a cacheable command.
756 766
757 767 Returns an ``iwireprotocolcommandcacher`` instance.
758 768
759 769 Extensions can monkeypatch this function to provide custom caching
760 770 backends.
761 771 """
762 772 return None
763 773
764 774 @wireprotocommand('branchmap', permission='pull')
765 775 def branchmapv2(repo, proto):
766 776 yield {encoding.fromlocal(k): v
767 777 for k, v in repo.branchmap().iteritems()}
768 778
769 779 @wireprotocommand('capabilities', permission='pull')
770 780 def capabilitiesv2(repo, proto):
771 781 yield _capabilitiesv2(repo, proto)
772 782
773 783 @wireprotocommand(
774 784 'changesetdata',
775 785 args={
776 786 'noderange': {
777 787 'type': 'list',
778 788 'default': lambda: None,
779 789 'example': [[b'0123456...'], [b'abcdef...']],
780 790 },
781 791 'nodes': {
782 792 'type': 'list',
783 793 'default': lambda: None,
784 794 'example': [b'0123456...'],
785 795 },
786 796 'nodesdepth': {
787 797 'type': 'int',
788 798 'default': lambda: None,
789 799 'example': 10,
790 800 },
791 801 'fields': {
792 802 'type': 'set',
793 803 'default': set,
794 804 'example': {b'parents', b'revision'},
795 805 'validvalues': {b'bookmarks', b'parents', b'phase', b'revision'},
796 806 },
797 807 },
798 808 permission='pull')
799 809 def changesetdata(repo, proto, noderange, nodes, nodesdepth, fields):
800 810 # TODO look for unknown fields and abort when they can't be serviced.
801 811 # This could probably be validated by dispatcher using validvalues.
802 812
803 813 if noderange is None and nodes is None:
804 814 raise error.WireprotoCommandError(
805 815 'noderange or nodes must be defined')
806 816
807 817 if nodesdepth is not None and nodes is None:
808 818 raise error.WireprotoCommandError(
809 819 'nodesdepth requires the nodes argument')
810 820
811 821 if noderange is not None:
812 822 if len(noderange) != 2:
813 823 raise error.WireprotoCommandError(
814 824 'noderange must consist of 2 elements')
815 825
816 826 if not noderange[1]:
817 827 raise error.WireprotoCommandError(
818 828 'heads in noderange request cannot be empty')
819 829
820 830 cl = repo.changelog
821 831 hasnode = cl.hasnode
822 832
823 833 seen = set()
824 834 outgoing = []
825 835
826 836 if nodes is not None:
827 837 outgoing = [n for n in nodes if hasnode(n)]
828 838
829 839 if nodesdepth:
830 840 outgoing = [cl.node(r) for r in
831 841 repo.revs(b'ancestors(%ln, %d)', outgoing,
832 842 nodesdepth - 1)]
833 843
834 844 seen |= set(outgoing)
835 845
836 846 if noderange is not None:
837 847 if noderange[0]:
838 848 common = [n for n in noderange[0] if hasnode(n)]
839 849 else:
840 850 common = [nullid]
841 851
842 852 for n in discovery.outgoing(repo, common, noderange[1]).missing:
843 853 if n not in seen:
844 854 outgoing.append(n)
845 855 # Don't need to add to seen here because this is the final
846 856 # source of nodes and there should be no duplicates in this
847 857 # list.
848 858
849 859 seen.clear()
850 860 publishing = repo.publishing()
851 861
852 862 if outgoing:
853 863 repo.hook('preoutgoing', throw=True, source='serve')
854 864
855 865 yield {
856 866 b'totalitems': len(outgoing),
857 867 }
858 868
859 869 # The phases of nodes already transferred to the client may have changed
860 870 # since the client last requested data. We send phase-only records
861 871 # for these revisions, if requested.
862 872 if b'phase' in fields and noderange is not None:
863 873 # TODO skip nodes whose phase will be reflected by a node in the
864 874 # outgoing set. This is purely an optimization to reduce data
865 875 # size.
866 876 for node in noderange[0]:
867 877 yield {
868 878 b'node': node,
869 879 b'phase': b'public' if publishing else repo[node].phasestr()
870 880 }
871 881
872 882 nodebookmarks = {}
873 883 for mark, node in repo._bookmarks.items():
874 884 nodebookmarks.setdefault(node, set()).add(mark)
875 885
876 886 # It is already topologically sorted by revision number.
877 887 for node in outgoing:
878 888 d = {
879 889 b'node': node,
880 890 }
881 891
882 892 if b'parents' in fields:
883 893 d[b'parents'] = cl.parents(node)
884 894
885 895 if b'phase' in fields:
886 896 if publishing:
887 897 d[b'phase'] = b'public'
888 898 else:
889 899 ctx = repo[node]
890 900 d[b'phase'] = ctx.phasestr()
891 901
892 902 if b'bookmarks' in fields and node in nodebookmarks:
893 903 d[b'bookmarks'] = sorted(nodebookmarks[node])
894 904 del nodebookmarks[node]
895 905
896 906 followingmeta = []
897 907 followingdata = []
898 908
899 909 if b'revision' in fields:
900 910 revisiondata = cl.revision(node, raw=True)
901 911 followingmeta.append((b'revision', len(revisiondata)))
902 912 followingdata.append(revisiondata)
903 913
904 914 # TODO make it possible for extensions to wrap a function or register
905 915 # a handler to service custom fields.
906 916
907 917 if followingmeta:
908 918 d[b'fieldsfollowing'] = followingmeta
909 919
910 920 yield d
911 921
912 922 for extra in followingdata:
913 923 yield extra
914 924
915 925 # If requested, send bookmarks from nodes that didn't have revision
916 926 # data sent so receiver is aware of any bookmark updates.
917 927 if b'bookmarks' in fields:
918 928 for node, marks in sorted(nodebookmarks.iteritems()):
919 929 yield {
920 930 b'node': node,
921 931 b'bookmarks': sorted(marks),
922 932 }
923 933
924 934 class FileAccessError(Exception):
925 935 """Represents an error accessing a specific file."""
926 936
927 937 def __init__(self, path, msg, args):
928 938 self.path = path
929 939 self.msg = msg
930 940 self.args = args
931 941
932 942 def getfilestore(repo, proto, path):
933 943 """Obtain a file storage object for use with wire protocol.
934 944
935 945 Exists as a standalone function so extensions can monkeypatch to add
936 946 access control.
937 947 """
938 948 # This seems to work even if the file doesn't exist. So catch
939 949 # "empty" files and return an error.
940 950 fl = repo.file(path)
941 951
942 952 if not len(fl):
943 953 raise FileAccessError(path, 'unknown file: %s', (path,))
944 954
945 955 return fl
946 956
947 957 @wireprotocommand(
948 958 'filedata',
949 959 args={
950 960 'haveparents': {
951 961 'type': 'bool',
952 962 'default': lambda: False,
953 963 'example': True,
954 964 },
955 965 'nodes': {
956 966 'type': 'list',
957 967 'example': [b'0123456...'],
958 968 },
959 969 'fields': {
960 970 'type': 'set',
961 971 'default': set,
962 972 'example': {b'parents', b'revision'},
963 973 'validvalues': {b'parents', b'revision'},
964 974 },
965 975 'path': {
966 976 'type': 'bytes',
967 977 'example': b'foo.txt',
968 978 }
969 979 },
970 980 permission='pull',
971 981 # TODO censoring a file revision won't invalidate the cache.
972 982 # Figure out a way to take censoring into account when deriving
973 983 # the cache key.
974 984 cachekeyfn=makecommandcachekeyfn('filedata', 1, allargs=True))
975 985 def filedata(repo, proto, haveparents, nodes, fields, path):
976 986 try:
977 987 # Extensions may wish to access the protocol handler.
978 988 store = getfilestore(repo, proto, path)
979 989 except FileAccessError as e:
980 990 raise error.WireprotoCommandError(e.msg, e.args)
981 991
982 992 # Validate requested nodes.
983 993 for node in nodes:
984 994 try:
985 995 store.rev(node)
986 996 except error.LookupError:
987 997 raise error.WireprotoCommandError('unknown file node: %s',
988 998 (hex(node),))
989 999
990 1000 revisions = store.emitrevisions(nodes,
991 1001 revisiondata=b'revision' in fields,
992 1002 assumehaveparentrevisions=haveparents)
993 1003
994 1004 yield {
995 1005 b'totalitems': len(nodes),
996 1006 }
997 1007
998 1008 for revision in revisions:
999 1009 d = {
1000 1010 b'node': revision.node,
1001 1011 }
1002 1012
1003 1013 if b'parents' in fields:
1004 1014 d[b'parents'] = [revision.p1node, revision.p2node]
1005 1015
1006 1016 followingmeta = []
1007 1017 followingdata = []
1008 1018
1009 1019 if b'revision' in fields:
1010 1020 if revision.revision is not None:
1011 1021 followingmeta.append((b'revision', len(revision.revision)))
1012 1022 followingdata.append(revision.revision)
1013 1023 else:
1014 1024 d[b'deltabasenode'] = revision.basenode
1015 1025 followingmeta.append((b'delta', len(revision.delta)))
1016 1026 followingdata.append(revision.delta)
1017 1027
1018 1028 if followingmeta:
1019 1029 d[b'fieldsfollowing'] = followingmeta
1020 1030
1021 1031 yield d
1022 1032
1023 1033 for extra in followingdata:
1024 1034 yield extra
1025 1035
1026 1036 @wireprotocommand(
1027 1037 'heads',
1028 1038 args={
1029 1039 'publiconly': {
1030 1040 'type': 'bool',
1031 1041 'default': lambda: False,
1032 1042 'example': False,
1033 1043 },
1034 1044 },
1035 1045 permission='pull')
1036 1046 def headsv2(repo, proto, publiconly):
1037 1047 if publiconly:
1038 1048 repo = repo.filtered('immutable')
1039 1049
1040 1050 yield repo.heads()
1041 1051
1042 1052 @wireprotocommand(
1043 1053 'known',
1044 1054 args={
1045 1055 'nodes': {
1046 1056 'type': 'list',
1047 1057 'default': list,
1048 1058 'example': [b'deadbeef'],
1049 1059 },
1050 1060 },
1051 1061 permission='pull')
1052 1062 def knownv2(repo, proto, nodes):
1053 1063 result = b''.join(b'1' if n else b'0' for n in repo.known(nodes))
1054 1064 yield result
1055 1065
1056 1066 @wireprotocommand(
1057 1067 'listkeys',
1058 1068 args={
1059 1069 'namespace': {
1060 1070 'type': 'bytes',
1061 1071 'example': b'ns',
1062 1072 },
1063 1073 },
1064 1074 permission='pull')
1065 1075 def listkeysv2(repo, proto, namespace):
1066 1076 keys = repo.listkeys(encoding.tolocal(namespace))
1067 1077 keys = {encoding.fromlocal(k): encoding.fromlocal(v)
1068 1078 for k, v in keys.iteritems()}
1069 1079
1070 1080 yield keys
1071 1081
1072 1082 @wireprotocommand(
1073 1083 'lookup',
1074 1084 args={
1075 1085 'key': {
1076 1086 'type': 'bytes',
1077 1087 'example': b'foo',
1078 1088 },
1079 1089 },
1080 1090 permission='pull')
1081 1091 def lookupv2(repo, proto, key):
1082 1092 key = encoding.tolocal(key)
1083 1093
1084 1094 # TODO handle exception.
1085 1095 node = repo.lookup(key)
1086 1096
1087 1097 yield node
1088 1098
1089 1099 @wireprotocommand(
1090 1100 'manifestdata',
1091 1101 args={
1092 1102 'nodes': {
1093 1103 'type': 'list',
1094 1104 'example': [b'0123456...'],
1095 1105 },
1096 1106 'haveparents': {
1097 1107 'type': 'bool',
1098 1108 'default': lambda: False,
1099 1109 'example': True,
1100 1110 },
1101 1111 'fields': {
1102 1112 'type': 'set',
1103 1113 'default': set,
1104 1114 'example': {b'parents', b'revision'},
1105 1115 'validvalues': {b'parents', b'revision'},
1106 1116 },
1107 1117 'tree': {
1108 1118 'type': 'bytes',
1109 1119 'example': b'',
1110 1120 },
1111 1121 },
1112 1122 permission='pull',
1113 1123 cachekeyfn=makecommandcachekeyfn('manifestdata', 1, allargs=True))
1114 1124 def manifestdata(repo, proto, haveparents, nodes, fields, tree):
1115 1125 store = repo.manifestlog.getstorage(tree)
1116 1126
1117 1127 # Validate the node is known and abort on unknown revisions.
1118 1128 for node in nodes:
1119 1129 try:
1120 1130 store.rev(node)
1121 1131 except error.LookupError:
1122 1132 raise error.WireprotoCommandError(
1123 1133 'unknown node: %s', (node,))
1124 1134
1125 1135 revisions = store.emitrevisions(nodes,
1126 1136 revisiondata=b'revision' in fields,
1127 1137 assumehaveparentrevisions=haveparents)
1128 1138
1129 1139 yield {
1130 1140 b'totalitems': len(nodes),
1131 1141 }
1132 1142
1133 1143 for revision in revisions:
1134 1144 d = {
1135 1145 b'node': revision.node,
1136 1146 }
1137 1147
1138 1148 if b'parents' in fields:
1139 1149 d[b'parents'] = [revision.p1node, revision.p2node]
1140 1150
1141 1151 followingmeta = []
1142 1152 followingdata = []
1143 1153
1144 1154 if b'revision' in fields:
1145 1155 if revision.revision is not None:
1146 1156 followingmeta.append((b'revision', len(revision.revision)))
1147 1157 followingdata.append(revision.revision)
1148 1158 else:
1149 1159 d[b'deltabasenode'] = revision.basenode
1150 1160 followingmeta.append((b'delta', len(revision.delta)))
1151 1161 followingdata.append(revision.delta)
1152 1162
1153 1163 if followingmeta:
1154 1164 d[b'fieldsfollowing'] = followingmeta
1155 1165
1156 1166 yield d
1157 1167
1158 1168 for extra in followingdata:
1159 1169 yield extra
1160 1170
1161 1171 @wireprotocommand(
1162 1172 'pushkey',
1163 1173 args={
1164 1174 'namespace': {
1165 1175 'type': 'bytes',
1166 1176 'example': b'ns',
1167 1177 },
1168 1178 'key': {
1169 1179 'type': 'bytes',
1170 1180 'example': b'key',
1171 1181 },
1172 1182 'old': {
1173 1183 'type': 'bytes',
1174 1184 'example': b'old',
1175 1185 },
1176 1186 'new': {
1177 1187 'type': 'bytes',
1178 1188 'example': 'new',
1179 1189 },
1180 1190 },
1181 1191 permission='push')
1182 1192 def pushkeyv2(repo, proto, namespace, key, old, new):
1183 1193 # TODO handle ui output redirection
1184 1194 yield repo.pushkey(encoding.tolocal(namespace),
1185 1195 encoding.tolocal(key),
1186 1196 encoding.tolocal(old),
1187 1197 encoding.tolocal(new))
@@ -1,619 +1,619 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-0002 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-0002 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-0002/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-0002/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-0002/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-0002/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-0002/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-0002/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> 13\r\n
180 180 s> \x0b\x00\x00\x01\x00\x02\x011\xa1FstatusBok
181 181 s> \r\n
182 182 s> 27\r\n
183 183 s> \x1f\x00\x00\x01\x00\x02\x001X\x1dcustomreadonly bytes response
184 184 s> \r\n
185 185 s> 8\r\n
186 186 s> \x00\x00\x00\x01\x00\x02\x002
187 187 s> \r\n
188 188 s> 0\r\n
189 189 s> \r\n
190 190
191 191 $ sendhttpv2peer << EOF
192 192 > command customreadonly
193 193 > EOF
194 194 creating http peer for wire protocol version 2
195 195 sending customreadonly command
196 196 s> POST /api/exp-http-v2-0002/ro/customreadonly HTTP/1.1\r\n
197 197 s> Accept-Encoding: identity\r\n
198 198 s> accept: application/mercurial-exp-framing-0005\r\n
199 199 s> content-type: application/mercurial-exp-framing-0005\r\n
200 200 s> content-length: 29\r\n
201 201 s> host: $LOCALIP:$HGPORT\r\n (glob)
202 202 s> user-agent: Mercurial debugwireproto\r\n
203 203 s> \r\n
204 204 s> \x15\x00\x00\x01\x00\x01\x01\x11\xa1DnameNcustomreadonly
205 205 s> makefile('rb', None)
206 206 s> HTTP/1.1 200 OK\r\n
207 207 s> Server: testing stub value\r\n
208 208 s> Date: $HTTP_DATE$\r\n
209 209 s> Content-Type: application/mercurial-exp-framing-0005\r\n
210 210 s> Transfer-Encoding: chunked\r\n
211 211 s> \r\n
212 212 s> 13\r\n
213 213 s> \x0b\x00\x00\x01\x00\x02\x011
214 214 s> \xa1FstatusBok
215 215 s> \r\n
216 216 received frame(size=11; request=1; stream=2; streamflags=stream-begin; type=command-response; flags=continuation)
217 217 s> 27\r\n
218 218 s> \x1f\x00\x00\x01\x00\x02\x001
219 219 s> X\x1dcustomreadonly bytes response
220 220 s> \r\n
221 221 received frame(size=31; request=1; stream=2; streamflags=; type=command-response; flags=continuation)
222 222 s> 8\r\n
223 223 s> \x00\x00\x00\x01\x00\x02\x002
224 224 s> \r\n
225 225 s> 0\r\n
226 226 s> \r\n
227 227 received frame(size=0; request=1; stream=2; streamflags=; type=command-response; flags=eos)
228 228 response: gen[
229 229 b'customreadonly bytes response'
230 230 ]
231 231
232 232 Request to read-write command fails because server is read-only by default
233 233
234 234 GET to read-write request yields 405
235 235
236 236 $ sendhttpraw << EOF
237 237 > httprequest GET api/$HTTPV2/rw/customreadonly
238 238 > user-agent: test
239 239 > EOF
240 240 using raw connection to peer
241 241 s> GET /api/exp-http-v2-0002/rw/customreadonly HTTP/1.1\r\n
242 242 s> Accept-Encoding: identity\r\n
243 243 s> user-agent: test\r\n
244 244 s> host: $LOCALIP:$HGPORT\r\n (glob)
245 245 s> \r\n
246 246 s> makefile('rb', None)
247 247 s> HTTP/1.1 405 Method Not Allowed\r\n
248 248 s> Server: testing stub value\r\n
249 249 s> Date: $HTTP_DATE$\r\n
250 250 s> Allow: POST\r\n
251 251 s> Content-Length: 30\r\n
252 252 s> \r\n
253 253 s> commands require POST requests
254 254
255 255 Even for unknown commands
256 256
257 257 $ sendhttpraw << EOF
258 258 > httprequest GET api/$HTTPV2/rw/badcommand
259 259 > user-agent: test
260 260 > EOF
261 261 using raw connection to peer
262 262 s> GET /api/exp-http-v2-0002/rw/badcommand HTTP/1.1\r\n
263 263 s> Accept-Encoding: identity\r\n
264 264 s> user-agent: test\r\n
265 265 s> host: $LOCALIP:$HGPORT\r\n (glob)
266 266 s> \r\n
267 267 s> makefile('rb', None)
268 268 s> HTTP/1.1 405 Method Not Allowed\r\n
269 269 s> Server: testing stub value\r\n
270 270 s> Date: $HTTP_DATE$\r\n
271 271 s> Allow: POST\r\n
272 272 s> Content-Length: 30\r\n
273 273 s> \r\n
274 274 s> commands require POST requests
275 275
276 276 SSL required by default
277 277
278 278 $ sendhttpraw << EOF
279 279 > httprequest POST api/$HTTPV2/rw/customreadonly
280 280 > user-agent: test
281 281 > EOF
282 282 using raw connection to peer
283 283 s> POST /api/exp-http-v2-0002/rw/customreadonly HTTP/1.1\r\n
284 284 s> Accept-Encoding: identity\r\n
285 285 s> user-agent: test\r\n
286 286 s> host: $LOCALIP:$HGPORT\r\n (glob)
287 287 s> \r\n
288 288 s> makefile('rb', None)
289 289 s> HTTP/1.1 403 ssl required\r\n
290 290 s> Server: testing stub value\r\n
291 291 s> Date: $HTTP_DATE$\r\n
292 292 s> Content-Length: 17\r\n
293 293 s> \r\n
294 294 s> permission denied
295 295
296 296 Restart server to allow non-ssl read-write operations
297 297
298 298 $ killdaemons.py
299 299 $ cat > server/.hg/hgrc << EOF
300 300 > [experimental]
301 301 > web.apiserver = true
302 302 > web.api.http-v2 = true
303 303 > [web]
304 304 > push_ssl = false
305 305 > allow-push = *
306 306 > EOF
307 307
308 308 $ hg -R server serve -p $HGPORT -d --pid-file hg.pid -E error.log
309 309 $ cat hg.pid > $DAEMON_PIDS
310 310
311 311 Authorized request for valid read-write command works
312 312
313 313 $ sendhttpraw << EOF
314 314 > httprequest POST api/$HTTPV2/rw/customreadonly
315 315 > user-agent: test
316 316 > accept: $MEDIATYPE
317 317 > content-type: $MEDIATYPE
318 318 > frame 1 1 stream-begin command-request new cbor:{b'name': b'customreadonly'}
319 319 > EOF
320 320 using raw connection to peer
321 321 s> POST /api/exp-http-v2-0002/rw/customreadonly HTTP/1.1\r\n
322 322 s> Accept-Encoding: identity\r\n
323 323 s> accept: application/mercurial-exp-framing-0005\r\n
324 324 s> content-type: application/mercurial-exp-framing-0005\r\n
325 325 s> user-agent: test\r\n
326 326 s> content-length: 29\r\n
327 327 s> host: $LOCALIP:$HGPORT\r\n (glob)
328 328 s> \r\n
329 329 s> \x15\x00\x00\x01\x00\x01\x01\x11\xa1DnameNcustomreadonly
330 330 s> makefile('rb', None)
331 331 s> HTTP/1.1 200 OK\r\n
332 332 s> Server: testing stub value\r\n
333 333 s> Date: $HTTP_DATE$\r\n
334 334 s> Content-Type: application/mercurial-exp-framing-0005\r\n
335 335 s> Transfer-Encoding: chunked\r\n
336 336 s> \r\n
337 337 s> 13\r\n
338 338 s> \x0b\x00\x00\x01\x00\x02\x011\xa1FstatusBok
339 339 s> \r\n
340 340 s> 27\r\n
341 341 s> \x1f\x00\x00\x01\x00\x02\x001X\x1dcustomreadonly bytes response
342 342 s> \r\n
343 343 s> 8\r\n
344 344 s> \x00\x00\x00\x01\x00\x02\x002
345 345 s> \r\n
346 346 s> 0\r\n
347 347 s> \r\n
348 348
349 349 Authorized request for unknown command is rejected
350 350
351 351 $ sendhttpraw << EOF
352 352 > httprequest POST api/$HTTPV2/rw/badcommand
353 353 > user-agent: test
354 354 > accept: $MEDIATYPE
355 355 > EOF
356 356 using raw connection to peer
357 357 s> POST /api/exp-http-v2-0002/rw/badcommand HTTP/1.1\r\n
358 358 s> Accept-Encoding: identity\r\n
359 359 s> accept: application/mercurial-exp-framing-0005\r\n
360 360 s> user-agent: test\r\n
361 361 s> host: $LOCALIP:$HGPORT\r\n (glob)
362 362 s> \r\n
363 363 s> makefile('rb', None)
364 364 s> HTTP/1.1 404 Not Found\r\n
365 365 s> Server: testing stub value\r\n
366 366 s> Date: $HTTP_DATE$\r\n
367 367 s> Content-Type: text/plain\r\n
368 368 s> Content-Length: 42\r\n
369 369 s> \r\n
370 370 s> unknown wire protocol command: badcommand\n
371 371
372 372 debugreflect isn't enabled by default
373 373
374 374 $ sendhttpraw << EOF
375 375 > httprequest POST api/$HTTPV2/ro/debugreflect
376 376 > user-agent: test
377 377 > EOF
378 378 using raw connection to peer
379 379 s> POST /api/exp-http-v2-0002/ro/debugreflect HTTP/1.1\r\n
380 380 s> Accept-Encoding: identity\r\n
381 381 s> user-agent: test\r\n
382 382 s> host: $LOCALIP:$HGPORT\r\n (glob)
383 383 s> \r\n
384 384 s> makefile('rb', None)
385 385 s> HTTP/1.1 404 Not Found\r\n
386 386 s> Server: testing stub value\r\n
387 387 s> Date: $HTTP_DATE$\r\n
388 388 s> Content-Type: text/plain\r\n
389 389 s> Content-Length: 34\r\n
390 390 s> \r\n
391 391 s> debugreflect service not available
392 392
393 393 Restart server to get debugreflect endpoint
394 394
395 395 $ killdaemons.py
396 396 $ cat > server/.hg/hgrc << EOF
397 397 > [experimental]
398 398 > web.apiserver = true
399 399 > web.api.debugreflect = true
400 400 > web.api.http-v2 = true
401 401 > [web]
402 402 > push_ssl = false
403 403 > allow-push = *
404 404 > EOF
405 405
406 406 $ hg -R server serve -p $HGPORT -d --pid-file hg.pid -E error.log
407 407 $ cat hg.pid > $DAEMON_PIDS
408 408
409 409 Command frames can be reflected via debugreflect
410 410
411 411 $ sendhttpraw << EOF
412 412 > httprequest POST api/$HTTPV2/ro/debugreflect
413 413 > accept: $MEDIATYPE
414 414 > content-type: $MEDIATYPE
415 415 > user-agent: test
416 416 > frame 1 1 stream-begin command-request new cbor:{b'name': b'command1', b'args': {b'foo': b'val1', b'bar1': b'val'}}
417 417 > EOF
418 418 using raw connection to peer
419 419 s> POST /api/exp-http-v2-0002/ro/debugreflect HTTP/1.1\r\n
420 420 s> Accept-Encoding: identity\r\n
421 421 s> accept: application/mercurial-exp-framing-0005\r\n
422 422 s> content-type: application/mercurial-exp-framing-0005\r\n
423 423 s> user-agent: test\r\n
424 424 s> content-length: 47\r\n
425 425 s> host: $LOCALIP:$HGPORT\r\n (glob)
426 426 s> \r\n
427 427 s> \'\x00\x00\x01\x00\x01\x01\x11\xa2Dargs\xa2Dbar1CvalCfooDval1DnameHcommand1
428 428 s> makefile('rb', None)
429 429 s> HTTP/1.1 200 OK\r\n
430 430 s> Server: testing stub value\r\n
431 431 s> Date: $HTTP_DATE$\r\n
432 432 s> Content-Type: text/plain\r\n
433 s> Content-Length: 205\r\n
433 s> Content-Length: 223\r\n
434 434 s> \r\n
435 435 s> received: 1 1 1 \xa2Dargs\xa2Dbar1CvalCfooDval1DnameHcommand1\n
436 s> ["runcommand", {"args": {"bar1": "val", "foo": "val1"}, "command": "command1", "data": null, "requestid": 1}]\n
436 s> ["runcommand", {"args": {"bar1": "val", "foo": "val1"}, "command": "command1", "data": null, "redirect": null, "requestid": 1}]\n
437 437 s> received: <no frame>\n
438 438 s> {"action": "noop"}
439 439
440 440 Multiple requests to regular command URL are not allowed
441 441
442 442 $ sendhttpraw << EOF
443 443 > httprequest POST api/$HTTPV2/ro/customreadonly
444 444 > accept: $MEDIATYPE
445 445 > content-type: $MEDIATYPE
446 446 > user-agent: test
447 447 > frame 1 1 stream-begin command-request new cbor:{b'name': b'customreadonly'}
448 448 > EOF
449 449 using raw connection to peer
450 450 s> POST /api/exp-http-v2-0002/ro/customreadonly HTTP/1.1\r\n
451 451 s> Accept-Encoding: identity\r\n
452 452 s> accept: application/mercurial-exp-framing-0005\r\n
453 453 s> content-type: application/mercurial-exp-framing-0005\r\n
454 454 s> user-agent: test\r\n
455 455 s> content-length: 29\r\n
456 456 s> host: $LOCALIP:$HGPORT\r\n (glob)
457 457 s> \r\n
458 458 s> \x15\x00\x00\x01\x00\x01\x01\x11\xa1DnameNcustomreadonly
459 459 s> makefile('rb', None)
460 460 s> HTTP/1.1 200 OK\r\n
461 461 s> Server: testing stub value\r\n
462 462 s> Date: $HTTP_DATE$\r\n
463 463 s> Content-Type: application/mercurial-exp-framing-0005\r\n
464 464 s> Transfer-Encoding: chunked\r\n
465 465 s> \r\n
466 466 s> 13\r\n
467 467 s> \x0b\x00\x00\x01\x00\x02\x011\xa1FstatusBok
468 468 s> \r\n
469 469 s> 27\r\n
470 470 s> \x1f\x00\x00\x01\x00\x02\x001X\x1dcustomreadonly bytes response
471 471 s> \r\n
472 472 s> 8\r\n
473 473 s> \x00\x00\x00\x01\x00\x02\x002
474 474 s> \r\n
475 475 s> 0\r\n
476 476 s> \r\n
477 477
478 478 Multiple requests to "multirequest" URL are allowed
479 479
480 480 $ sendhttpraw << EOF
481 481 > httprequest POST api/$HTTPV2/ro/multirequest
482 482 > accept: $MEDIATYPE
483 483 > content-type: $MEDIATYPE
484 484 > user-agent: test
485 485 > frame 1 1 stream-begin command-request new cbor:{b'name': b'customreadonly'}
486 486 > frame 3 1 0 command-request new cbor:{b'name': b'customreadonly'}
487 487 > EOF
488 488 using raw connection to peer
489 489 s> POST /api/exp-http-v2-0002/ro/multirequest HTTP/1.1\r\n
490 490 s> Accept-Encoding: identity\r\n
491 491 s> *\r\n (glob)
492 492 s> *\r\n (glob)
493 493 s> user-agent: test\r\n
494 494 s> content-length: 58\r\n
495 495 s> host: $LOCALIP:$HGPORT\r\n (glob)
496 496 s> \r\n
497 497 s> \x15\x00\x00\x01\x00\x01\x01\x11\xa1DnameNcustomreadonly\x15\x00\x00\x03\x00\x01\x00\x11\xa1DnameNcustomreadonly
498 498 s> makefile('rb', None)
499 499 s> HTTP/1.1 200 OK\r\n
500 500 s> Server: testing stub value\r\n
501 501 s> Date: $HTTP_DATE$\r\n
502 502 s> Content-Type: application/mercurial-exp-framing-0005\r\n
503 503 s> Transfer-Encoding: chunked\r\n
504 504 s> \r\n
505 505 s> 13\r\n
506 506 s> \x0b\x00\x00\x01\x00\x02\x011\xa1FstatusBok
507 507 s> \r\n
508 508 s> 27\r\n
509 509 s> \x1f\x00\x00\x01\x00\x02\x001X\x1dcustomreadonly bytes response
510 510 s> \r\n
511 511 s> 8\r\n
512 512 s> \x00\x00\x00\x01\x00\x02\x002
513 513 s> \r\n
514 514 s> 13\r\n
515 515 s> \x0b\x00\x00\x03\x00\x02\x001\xa1FstatusBok
516 516 s> \r\n
517 517 s> 27\r\n
518 518 s> \x1f\x00\x00\x03\x00\x02\x001X\x1dcustomreadonly bytes response
519 519 s> \r\n
520 520 s> 8\r\n
521 521 s> \x00\x00\x00\x03\x00\x02\x002
522 522 s> \r\n
523 523 s> 0\r\n
524 524 s> \r\n
525 525
526 526 Interleaved requests to "multirequest" are processed
527 527
528 528 $ sendhttpraw << EOF
529 529 > httprequest POST api/$HTTPV2/ro/multirequest
530 530 > accept: $MEDIATYPE
531 531 > content-type: $MEDIATYPE
532 532 > user-agent: test
533 533 > frame 1 1 stream-begin command-request new|more \xa2Dargs\xa1Inamespace
534 534 > frame 3 1 0 command-request new|more \xa2Dargs\xa1Inamespace
535 535 > frame 3 1 0 command-request continuation JnamespacesDnameHlistkeys
536 536 > frame 1 1 0 command-request continuation IbookmarksDnameHlistkeys
537 537 > EOF
538 538 using raw connection to peer
539 539 s> POST /api/exp-http-v2-0002/ro/multirequest HTTP/1.1\r\n
540 540 s> Accept-Encoding: identity\r\n
541 541 s> accept: application/mercurial-exp-framing-0005\r\n
542 542 s> content-type: application/mercurial-exp-framing-0005\r\n
543 543 s> user-agent: test\r\n
544 544 s> content-length: 115\r\n
545 545 s> host: $LOCALIP:$HGPORT\r\n (glob)
546 546 s> \r\n
547 547 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
548 548 s> makefile('rb', None)
549 549 s> HTTP/1.1 200 OK\r\n
550 550 s> Server: testing stub value\r\n
551 551 s> Date: $HTTP_DATE$\r\n
552 552 s> Content-Type: application/mercurial-exp-framing-0005\r\n
553 553 s> Transfer-Encoding: chunked\r\n
554 554 s> \r\n
555 555 s> 13\r\n
556 556 s> \x0b\x00\x00\x03\x00\x02\x011\xa1FstatusBok
557 557 s> \r\n
558 558 s> 28\r\n
559 559 s> \x00\x00\x03\x00\x02\x001\xa3Ibookmarks@Jnamespaces@Fphases@
560 560 s> \r\n
561 561 s> 8\r\n
562 562 s> \x00\x00\x00\x03\x00\x02\x002
563 563 s> \r\n
564 564 s> 13\r\n
565 565 s> \x0b\x00\x00\x01\x00\x02\x001\xa1FstatusBok
566 566 s> \r\n
567 567 s> 9\r\n
568 568 s> \x01\x00\x00\x01\x00\x02\x001\xa0
569 569 s> \r\n
570 570 s> 8\r\n
571 571 s> \x00\x00\x00\x01\x00\x02\x002
572 572 s> \r\n
573 573 s> 0\r\n
574 574 s> \r\n
575 575
576 576 Restart server to disable read-write access
577 577
578 578 $ killdaemons.py
579 579 $ cat > server/.hg/hgrc << EOF
580 580 > [experimental]
581 581 > web.apiserver = true
582 582 > web.api.debugreflect = true
583 583 > web.api.http-v2 = true
584 584 > [web]
585 585 > push_ssl = false
586 586 > EOF
587 587
588 588 $ hg -R server serve -p $HGPORT -d --pid-file hg.pid -E error.log
589 589 $ cat hg.pid > $DAEMON_PIDS
590 590
591 591 Attempting to run a read-write command via multirequest on read-only URL is not allowed
592 592
593 593 $ sendhttpraw << EOF
594 594 > httprequest POST api/$HTTPV2/ro/multirequest
595 595 > accept: $MEDIATYPE
596 596 > content-type: $MEDIATYPE
597 597 > user-agent: test
598 598 > frame 1 1 stream-begin command-request new cbor:{b'name': b'pushkey'}
599 599 > EOF
600 600 using raw connection to peer
601 601 s> POST /api/exp-http-v2-0002/ro/multirequest HTTP/1.1\r\n
602 602 s> Accept-Encoding: identity\r\n
603 603 s> accept: application/mercurial-exp-framing-0005\r\n
604 604 s> content-type: application/mercurial-exp-framing-0005\r\n
605 605 s> user-agent: test\r\n
606 606 s> content-length: 22\r\n
607 607 s> host: $LOCALIP:$HGPORT\r\n (glob)
608 608 s> \r\n
609 609 s> \x0e\x00\x00\x01\x00\x01\x01\x11\xa1DnameGpushkey
610 610 s> makefile('rb', None)
611 611 s> HTTP/1.1 403 Forbidden\r\n
612 612 s> Server: testing stub value\r\n
613 613 s> Date: $HTTP_DATE$\r\n
614 614 s> Content-Type: text/plain\r\n
615 615 s> Content-Length: 52\r\n
616 616 s> \r\n
617 617 s> insufficient permissions to execute command: pushkey
618 618
619 619 $ cat error.log
@@ -1,1187 +1,1369 b''
1 1 $ . $TESTDIR/wireprotohelpers.sh
2 2
3 $ cat >> $HGRCPATH << EOF
4 > [extensions]
5 > blackbox =
6 > [blackbox]
7 > track = simplecache
8 > EOF
9
3 10 $ hg init server
4 11 $ enablehttpv2 server
5 12 $ cd server
6 13 $ cat >> .hg/hgrc << EOF
7 14 > [extensions]
8 15 > simplecache = $TESTDIR/wireprotosimplecache.py
16 > [simplecache]
17 > cacheapi = true
9 18 > EOF
10 19
11 20 $ echo a0 > a
12 21 $ echo b0 > b
13 22 $ hg -q commit -A -m 'commit 0'
14 23 $ echo a1 > a
15 24 $ hg commit -m 'commit 1'
16 25
17 26 $ hg --debug debugindex -m
18 27 rev linkrev nodeid p1 p2
19 28 0 0 992f4779029a3df8d0666d00bb924f69634e2641 0000000000000000000000000000000000000000 0000000000000000000000000000000000000000
20 29 1 1 a988fb43583e871d1ed5750ee074c6d840bbbfc8 992f4779029a3df8d0666d00bb924f69634e2641 0000000000000000000000000000000000000000
21 30
22 31 $ hg --config simplecache.redirectsfile=redirects.py serve -p $HGPORT -d --pid-file hg.pid -E error.log
23 32 $ cat hg.pid > $DAEMON_PIDS
24 33
25 34 $ cat > redirects.py << EOF
26 35 > [
27 36 > {
28 37 > b'name': b'target-a',
29 38 > b'protocol': b'http',
30 39 > b'snirequired': False,
31 40 > b'tlsversions': [b'1.2', b'1.3'],
32 41 > b'uris': [b'http://example.com/'],
33 42 > },
34 43 > ]
35 44 > EOF
36 45
37 46 Redirect targets advertised when configured
38 47
39 48 $ sendhttpv2peerhandshake << EOF
40 49 > command capabilities
41 50 > EOF
42 51 creating http peer for wire protocol version 2
43 52 s> GET /?cmd=capabilities HTTP/1.1\r\n
44 53 s> Accept-Encoding: identity\r\n
45 54 s> vary: X-HgProto-1,X-HgUpgrade-1\r\n
46 55 s> x-hgproto-1: cbor\r\n
47 56 s> x-hgupgrade-1: exp-http-v2-0002\r\n
48 57 s> accept: application/mercurial-0.1\r\n
49 58 s> host: $LOCALIP:$HGPORT\r\n (glob)
50 59 s> user-agent: Mercurial debugwireproto\r\n
51 60 s> \r\n
52 61 s> makefile('rb', None)
53 62 s> HTTP/1.1 200 OK\r\n
54 63 s> Server: testing stub value\r\n
55 64 s> Date: $HTTP_DATE$\r\n
56 65 s> Content-Type: application/mercurial-cbor\r\n
57 66 s> Content-Length: 1970\r\n
58 67 s> \r\n
59 68 s> \xa3GapibaseDapi/Dapis\xa1Pexp-http-v2-0002\xa6Hcommands\xaaIbranchmap\xa2Dargs\xa0Kpermissions\x81DpullLcapabilities\xa2Dargs\xa0Kpermissions\x81DpullMchangesetdata\xa2Dargs\xa4Ffields\xa4Gdefault\xd9\x01\x02\x80Hrequired\xf4DtypeCsetKvalidvalues\xd9\x01\x02\x84IbookmarksGparentsEphaseHrevisionInoderange\xa3Gdefault\xf6Hrequired\xf4DtypeDlistEnodes\xa3Gdefault\xf6Hrequired\xf4DtypeDlistJnodesdepth\xa3Gdefault\xf6Hrequired\xf4DtypeCintKpermissions\x81DpullHfiledata\xa2Dargs\xa4Ffields\xa4Gdefault\xd9\x01\x02\x80Hrequired\xf4DtypeCsetKvalidvalues\xd9\x01\x02\x82GparentsHrevisionKhaveparents\xa3Gdefault\xf4Hrequired\xf4DtypeDboolEnodes\xa2Hrequired\xf5DtypeDlistDpath\xa2Hrequired\xf5DtypeEbytesKpermissions\x81DpullEheads\xa2Dargs\xa1Jpubliconly\xa3Gdefault\xf4Hrequired\xf4DtypeDboolKpermissions\x81DpullEknown\xa2Dargs\xa1Enodes\xa3Gdefault\x80Hrequired\xf4DtypeDlistKpermissions\x81DpullHlistkeys\xa2Dargs\xa1Inamespace\xa2Hrequired\xf5DtypeEbytesKpermissions\x81DpullFlookup\xa2Dargs\xa1Ckey\xa2Hrequired\xf5DtypeEbytesKpermissions\x81DpullLmanifestdata\xa2Dargs\xa4Ffields\xa4Gdefault\xd9\x01\x02\x80Hrequired\xf4DtypeCsetKvalidvalues\xd9\x01\x02\x82GparentsHrevisionKhaveparents\xa3Gdefault\xf4Hrequired\xf4DtypeDboolEnodes\xa2Hrequired\xf5DtypeDlistDtree\xa2Hrequired\xf5DtypeEbytesKpermissions\x81DpullGpushkey\xa2Dargs\xa4Ckey\xa2Hrequired\xf5DtypeEbytesInamespace\xa2Hrequired\xf5DtypeEbytesCnew\xa2Hrequired\xf5DtypeEbytesCold\xa2Hrequired\xf5DtypeEbytesKpermissions\x81DpushKcompression\x82\xa1DnameDzstd\xa1DnameDzlibQframingmediatypes\x81X&application/mercurial-exp-framing-0005Rpathfilterprefixes\xd9\x01\x02\x82Epath:Lrootfilesin:Nrawrepoformats\x82LgeneraldeltaHrevlogv1Hredirect\xa2Fhashes\x82Fsha256Dsha1Gtargets\x81\xa5DnameHtarget-aHprotocolDhttpKsnirequired\xf4Ktlsversions\x82C1.2C1.3Duris\x81Shttp://example.com/Nv1capabilitiesY\x01\xd8batch branchmap $USUAL_BUNDLE2_CAPS$ changegroupsubset compression=$BUNDLE2_COMPRESSIONS$ getbundle httpheader=1024 httpmediatype=0.1rx,0.1tx,0.2tx known lookup pushkey streamreqs=generaldelta,revlogv1 unbundle=HG10GZ,HG10BZ,HG10UN unbundlehash
60 69 (remote redirect target target-a is compatible)
61 70 sending capabilities command
62 71 s> POST /api/exp-http-v2-0002/ro/capabilities HTTP/1.1\r\n
63 72 s> Accept-Encoding: identity\r\n
64 73 s> accept: application/mercurial-exp-framing-0005\r\n
65 74 s> content-type: application/mercurial-exp-framing-0005\r\n
66 75 s> content-length: 75\r\n
67 76 s> host: $LOCALIP:$HGPORT\r\n (glob)
68 77 s> user-agent: Mercurial debugwireproto\r\n
69 78 s> \r\n
70 79 s> C\x00\x00\x01\x00\x01\x01\x11\xa2DnameLcapabilitiesHredirect\xa2Fhashes\x82Fsha256Dsha1Gtargets\x81Htarget-a
71 80 s> makefile('rb', None)
72 81 s> HTTP/1.1 200 OK\r\n
73 82 s> Server: testing stub value\r\n
74 83 s> Date: $HTTP_DATE$\r\n
75 84 s> Content-Type: application/mercurial-exp-framing-0005\r\n
76 85 s> Transfer-Encoding: chunked\r\n
77 86 s> \r\n
78 87 s> 13\r\n
79 88 s> \x0b\x00\x00\x01\x00\x02\x011
80 89 s> \xa1FstatusBok
81 90 s> \r\n
82 91 received frame(size=11; request=1; stream=2; streamflags=stream-begin; type=command-response; flags=continuation)
83 92 s> 5ab\r\n
84 93 s> \xa3\x05\x00\x01\x00\x02\x001
85 94 s> \xa6Hcommands\xaaIbranchmap\xa2Dargs\xa0Kpermissions\x81DpullLcapabilities\xa2Dargs\xa0Kpermissions\x81DpullMchangesetdata\xa2Dargs\xa4Ffields\xa4Gdefault\xd9\x01\x02\x80Hrequired\xf4DtypeCsetKvalidvalues\xd9\x01\x02\x84IbookmarksGparentsEphaseHrevisionInoderange\xa3Gdefault\xf6Hrequired\xf4DtypeDlistEnodes\xa3Gdefault\xf6Hrequired\xf4DtypeDlistJnodesdepth\xa3Gdefault\xf6Hrequired\xf4DtypeCintKpermissions\x81DpullHfiledata\xa2Dargs\xa4Ffields\xa4Gdefault\xd9\x01\x02\x80Hrequired\xf4DtypeCsetKvalidvalues\xd9\x01\x02\x82GparentsHrevisionKhaveparents\xa3Gdefault\xf4Hrequired\xf4DtypeDboolEnodes\xa2Hrequired\xf5DtypeDlistDpath\xa2Hrequired\xf5DtypeEbytesKpermissions\x81DpullEheads\xa2Dargs\xa1Jpubliconly\xa3Gdefault\xf4Hrequired\xf4DtypeDboolKpermissions\x81DpullEknown\xa2Dargs\xa1Enodes\xa3Gdefault\x80Hrequired\xf4DtypeDlistKpermissions\x81DpullHlistkeys\xa2Dargs\xa1Inamespace\xa2Hrequired\xf5DtypeEbytesKpermissions\x81DpullFlookup\xa2Dargs\xa1Ckey\xa2Hrequired\xf5DtypeEbytesKpermissions\x81DpullLmanifestdata\xa2Dargs\xa4Ffields\xa4Gdefault\xd9\x01\x02\x80Hrequired\xf4DtypeCsetKvalidvalues\xd9\x01\x02\x82GparentsHrevisionKhaveparents\xa3Gdefault\xf4Hrequired\xf4DtypeDboolEnodes\xa2Hrequired\xf5DtypeDlistDtree\xa2Hrequired\xf5DtypeEbytesKpermissions\x81DpullGpushkey\xa2Dargs\xa4Ckey\xa2Hrequired\xf5DtypeEbytesInamespace\xa2Hrequired\xf5DtypeEbytesCnew\xa2Hrequired\xf5DtypeEbytesCold\xa2Hrequired\xf5DtypeEbytesKpermissions\x81DpushKcompression\x82\xa1DnameDzstd\xa1DnameDzlibQframingmediatypes\x81X&application/mercurial-exp-framing-0005Rpathfilterprefixes\xd9\x01\x02\x82Epath:Lrootfilesin:Nrawrepoformats\x82LgeneraldeltaHrevlogv1Hredirect\xa2Fhashes\x82Fsha256Dsha1Gtargets\x81\xa5DnameHtarget-aHprotocolDhttpKsnirequired\xf4Ktlsversions\x82C1.2C1.3Duris\x81Shttp://example.com/
86 95 s> \r\n
87 96 received frame(size=1443; request=1; stream=2; streamflags=; type=command-response; flags=continuation)
88 97 s> 8\r\n
89 98 s> \x00\x00\x00\x01\x00\x02\x002
90 99 s> \r\n
91 100 s> 0\r\n
92 101 s> \r\n
93 102 received frame(size=0; request=1; stream=2; streamflags=; type=command-response; flags=eos)
94 103 response: gen[
95 104 {
96 105 b'commands': {
97 106 b'branchmap': {
98 107 b'args': {},
99 108 b'permissions': [
100 109 b'pull'
101 110 ]
102 111 },
103 112 b'capabilities': {
104 113 b'args': {},
105 114 b'permissions': [
106 115 b'pull'
107 116 ]
108 117 },
109 118 b'changesetdata': {
110 119 b'args': {
111 120 b'fields': {
112 121 b'default': set([]),
113 122 b'required': False,
114 123 b'type': b'set',
115 124 b'validvalues': set([
116 125 b'bookmarks',
117 126 b'parents',
118 127 b'phase',
119 128 b'revision'
120 129 ])
121 130 },
122 131 b'noderange': {
123 132 b'default': None,
124 133 b'required': False,
125 134 b'type': b'list'
126 135 },
127 136 b'nodes': {
128 137 b'default': None,
129 138 b'required': False,
130 139 b'type': b'list'
131 140 },
132 141 b'nodesdepth': {
133 142 b'default': None,
134 143 b'required': False,
135 144 b'type': b'int'
136 145 }
137 146 },
138 147 b'permissions': [
139 148 b'pull'
140 149 ]
141 150 },
142 151 b'filedata': {
143 152 b'args': {
144 153 b'fields': {
145 154 b'default': set([]),
146 155 b'required': False,
147 156 b'type': b'set',
148 157 b'validvalues': set([
149 158 b'parents',
150 159 b'revision'
151 160 ])
152 161 },
153 162 b'haveparents': {
154 163 b'default': False,
155 164 b'required': False,
156 165 b'type': b'bool'
157 166 },
158 167 b'nodes': {
159 168 b'required': True,
160 169 b'type': b'list'
161 170 },
162 171 b'path': {
163 172 b'required': True,
164 173 b'type': b'bytes'
165 174 }
166 175 },
167 176 b'permissions': [
168 177 b'pull'
169 178 ]
170 179 },
171 180 b'heads': {
172 181 b'args': {
173 182 b'publiconly': {
174 183 b'default': False,
175 184 b'required': False,
176 185 b'type': b'bool'
177 186 }
178 187 },
179 188 b'permissions': [
180 189 b'pull'
181 190 ]
182 191 },
183 192 b'known': {
184 193 b'args': {
185 194 b'nodes': {
186 195 b'default': [],
187 196 b'required': False,
188 197 b'type': b'list'
189 198 }
190 199 },
191 200 b'permissions': [
192 201 b'pull'
193 202 ]
194 203 },
195 204 b'listkeys': {
196 205 b'args': {
197 206 b'namespace': {
198 207 b'required': True,
199 208 b'type': b'bytes'
200 209 }
201 210 },
202 211 b'permissions': [
203 212 b'pull'
204 213 ]
205 214 },
206 215 b'lookup': {
207 216 b'args': {
208 217 b'key': {
209 218 b'required': True,
210 219 b'type': b'bytes'
211 220 }
212 221 },
213 222 b'permissions': [
214 223 b'pull'
215 224 ]
216 225 },
217 226 b'manifestdata': {
218 227 b'args': {
219 228 b'fields': {
220 229 b'default': set([]),
221 230 b'required': False,
222 231 b'type': b'set',
223 232 b'validvalues': set([
224 233 b'parents',
225 234 b'revision'
226 235 ])
227 236 },
228 237 b'haveparents': {
229 238 b'default': False,
230 239 b'required': False,
231 240 b'type': b'bool'
232 241 },
233 242 b'nodes': {
234 243 b'required': True,
235 244 b'type': b'list'
236 245 },
237 246 b'tree': {
238 247 b'required': True,
239 248 b'type': b'bytes'
240 249 }
241 250 },
242 251 b'permissions': [
243 252 b'pull'
244 253 ]
245 254 },
246 255 b'pushkey': {
247 256 b'args': {
248 257 b'key': {
249 258 b'required': True,
250 259 b'type': b'bytes'
251 260 },
252 261 b'namespace': {
253 262 b'required': True,
254 263 b'type': b'bytes'
255 264 },
256 265 b'new': {
257 266 b'required': True,
258 267 b'type': b'bytes'
259 268 },
260 269 b'old': {
261 270 b'required': True,
262 271 b'type': b'bytes'
263 272 }
264 273 },
265 274 b'permissions': [
266 275 b'push'
267 276 ]
268 277 }
269 278 },
270 279 b'compression': [
271 280 {
272 281 b'name': b'zstd'
273 282 },
274 283 {
275 284 b'name': b'zlib'
276 285 }
277 286 ],
278 287 b'framingmediatypes': [
279 288 b'application/mercurial-exp-framing-0005'
280 289 ],
281 290 b'pathfilterprefixes': set([
282 291 b'path:',
283 292 b'rootfilesin:'
284 293 ]),
285 294 b'rawrepoformats': [
286 295 b'generaldelta',
287 296 b'revlogv1'
288 297 ],
289 298 b'redirect': {
290 299 b'hashes': [
291 300 b'sha256',
292 301 b'sha1'
293 302 ],
294 303 b'targets': [
295 304 {
296 305 b'name': b'target-a',
297 306 b'protocol': b'http',
298 307 b'snirequired': False,
299 308 b'tlsversions': [
300 309 b'1.2',
301 310 b'1.3'
302 311 ],
303 312 b'uris': [
304 313 b'http://example.com/'
305 314 ]
306 315 }
307 316 ]
308 317 }
309 318 }
310 319 ]
311 320
312 321 Unknown protocol is filtered from compatible targets
313 322
314 323 $ cat > redirects.py << EOF
315 324 > [
316 325 > {
317 326 > b'name': b'target-a',
318 327 > b'protocol': b'http',
319 328 > b'uris': [b'http://example.com/'],
320 329 > },
321 330 > {
322 331 > b'name': b'target-b',
323 332 > b'protocol': b'unknown',
324 333 > b'uris': [b'unknown://example.com/'],
325 334 > },
326 335 > ]
327 336 > EOF
328 337
329 338 $ sendhttpv2peerhandshake << EOF
330 339 > command capabilities
331 340 > EOF
332 341 creating http peer for wire protocol version 2
333 342 s> GET /?cmd=capabilities HTTP/1.1\r\n
334 343 s> Accept-Encoding: identity\r\n
335 344 s> vary: X-HgProto-1,X-HgUpgrade-1\r\n
336 345 s> x-hgproto-1: cbor\r\n
337 346 s> x-hgupgrade-1: exp-http-v2-0002\r\n
338 347 s> accept: application/mercurial-0.1\r\n
339 348 s> host: $LOCALIP:$HGPORT\r\n (glob)
340 349 s> user-agent: Mercurial debugwireproto\r\n
341 350 s> \r\n
342 351 s> makefile('rb', None)
343 352 s> HTTP/1.1 200 OK\r\n
344 353 s> Server: testing stub value\r\n
345 354 s> Date: $HTTP_DATE$\r\n
346 355 s> Content-Type: application/mercurial-cbor\r\n
347 356 s> Content-Length: 1997\r\n
348 357 s> \r\n
349 358 s> \xa3GapibaseDapi/Dapis\xa1Pexp-http-v2-0002\xa6Hcommands\xaaIbranchmap\xa2Dargs\xa0Kpermissions\x81DpullLcapabilities\xa2Dargs\xa0Kpermissions\x81DpullMchangesetdata\xa2Dargs\xa4Ffields\xa4Gdefault\xd9\x01\x02\x80Hrequired\xf4DtypeCsetKvalidvalues\xd9\x01\x02\x84IbookmarksGparentsEphaseHrevisionInoderange\xa3Gdefault\xf6Hrequired\xf4DtypeDlistEnodes\xa3Gdefault\xf6Hrequired\xf4DtypeDlistJnodesdepth\xa3Gdefault\xf6Hrequired\xf4DtypeCintKpermissions\x81DpullHfiledata\xa2Dargs\xa4Ffields\xa4Gdefault\xd9\x01\x02\x80Hrequired\xf4DtypeCsetKvalidvalues\xd9\x01\x02\x82GparentsHrevisionKhaveparents\xa3Gdefault\xf4Hrequired\xf4DtypeDboolEnodes\xa2Hrequired\xf5DtypeDlistDpath\xa2Hrequired\xf5DtypeEbytesKpermissions\x81DpullEheads\xa2Dargs\xa1Jpubliconly\xa3Gdefault\xf4Hrequired\xf4DtypeDboolKpermissions\x81DpullEknown\xa2Dargs\xa1Enodes\xa3Gdefault\x80Hrequired\xf4DtypeDlistKpermissions\x81DpullHlistkeys\xa2Dargs\xa1Inamespace\xa2Hrequired\xf5DtypeEbytesKpermissions\x81DpullFlookup\xa2Dargs\xa1Ckey\xa2Hrequired\xf5DtypeEbytesKpermissions\x81DpullLmanifestdata\xa2Dargs\xa4Ffields\xa4Gdefault\xd9\x01\x02\x80Hrequired\xf4DtypeCsetKvalidvalues\xd9\x01\x02\x82GparentsHrevisionKhaveparents\xa3Gdefault\xf4Hrequired\xf4DtypeDboolEnodes\xa2Hrequired\xf5DtypeDlistDtree\xa2Hrequired\xf5DtypeEbytesKpermissions\x81DpullGpushkey\xa2Dargs\xa4Ckey\xa2Hrequired\xf5DtypeEbytesInamespace\xa2Hrequired\xf5DtypeEbytesCnew\xa2Hrequired\xf5DtypeEbytesCold\xa2Hrequired\xf5DtypeEbytesKpermissions\x81DpushKcompression\x82\xa1DnameDzstd\xa1DnameDzlibQframingmediatypes\x81X&application/mercurial-exp-framing-0005Rpathfilterprefixes\xd9\x01\x02\x82Epath:Lrootfilesin:Nrawrepoformats\x82LgeneraldeltaHrevlogv1Hredirect\xa2Fhashes\x82Fsha256Dsha1Gtargets\x82\xa3DnameHtarget-aHprotocolDhttpDuris\x81Shttp://example.com/\xa3DnameHtarget-bHprotocolGunknownDuris\x81Vunknown://example.com/Nv1capabilitiesY\x01\xd8batch branchmap $USUAL_BUNDLE2_CAPS$ changegroupsubset compression=$BUNDLE2_COMPRESSIONS$ getbundle httpheader=1024 httpmediatype=0.1rx,0.1tx,0.2tx known lookup pushkey streamreqs=generaldelta,revlogv1 unbundle=HG10GZ,HG10BZ,HG10UN unbundlehash
350 359 (remote redirect target target-a is compatible)
351 360 (remote redirect target target-b uses unsupported protocol: unknown)
352 361 sending capabilities command
353 362 s> POST /api/exp-http-v2-0002/ro/capabilities HTTP/1.1\r\n
354 363 s> Accept-Encoding: identity\r\n
355 364 s> accept: application/mercurial-exp-framing-0005\r\n
356 365 s> content-type: application/mercurial-exp-framing-0005\r\n
357 366 s> content-length: 75\r\n
358 367 s> host: $LOCALIP:$HGPORT\r\n (glob)
359 368 s> user-agent: Mercurial debugwireproto\r\n
360 369 s> \r\n
361 370 s> C\x00\x00\x01\x00\x01\x01\x11\xa2DnameLcapabilitiesHredirect\xa2Fhashes\x82Fsha256Dsha1Gtargets\x81Htarget-a
362 371 s> makefile('rb', None)
363 372 s> HTTP/1.1 200 OK\r\n
364 373 s> Server: testing stub value\r\n
365 374 s> Date: $HTTP_DATE$\r\n
366 375 s> Content-Type: application/mercurial-exp-framing-0005\r\n
367 376 s> Transfer-Encoding: chunked\r\n
368 377 s> \r\n
369 378 s> 13\r\n
370 379 s> \x0b\x00\x00\x01\x00\x02\x011
371 380 s> \xa1FstatusBok
372 381 s> \r\n
373 382 received frame(size=11; request=1; stream=2; streamflags=stream-begin; type=command-response; flags=continuation)
374 383 s> 5c6\r\n
375 384 s> \xbe\x05\x00\x01\x00\x02\x001
376 385 s> \xa6Hcommands\xaaIbranchmap\xa2Dargs\xa0Kpermissions\x81DpullLcapabilities\xa2Dargs\xa0Kpermissions\x81DpullMchangesetdata\xa2Dargs\xa4Ffields\xa4Gdefault\xd9\x01\x02\x80Hrequired\xf4DtypeCsetKvalidvalues\xd9\x01\x02\x84IbookmarksGparentsEphaseHrevisionInoderange\xa3Gdefault\xf6Hrequired\xf4DtypeDlistEnodes\xa3Gdefault\xf6Hrequired\xf4DtypeDlistJnodesdepth\xa3Gdefault\xf6Hrequired\xf4DtypeCintKpermissions\x81DpullHfiledata\xa2Dargs\xa4Ffields\xa4Gdefault\xd9\x01\x02\x80Hrequired\xf4DtypeCsetKvalidvalues\xd9\x01\x02\x82GparentsHrevisionKhaveparents\xa3Gdefault\xf4Hrequired\xf4DtypeDboolEnodes\xa2Hrequired\xf5DtypeDlistDpath\xa2Hrequired\xf5DtypeEbytesKpermissions\x81DpullEheads\xa2Dargs\xa1Jpubliconly\xa3Gdefault\xf4Hrequired\xf4DtypeDboolKpermissions\x81DpullEknown\xa2Dargs\xa1Enodes\xa3Gdefault\x80Hrequired\xf4DtypeDlistKpermissions\x81DpullHlistkeys\xa2Dargs\xa1Inamespace\xa2Hrequired\xf5DtypeEbytesKpermissions\x81DpullFlookup\xa2Dargs\xa1Ckey\xa2Hrequired\xf5DtypeEbytesKpermissions\x81DpullLmanifestdata\xa2Dargs\xa4Ffields\xa4Gdefault\xd9\x01\x02\x80Hrequired\xf4DtypeCsetKvalidvalues\xd9\x01\x02\x82GparentsHrevisionKhaveparents\xa3Gdefault\xf4Hrequired\xf4DtypeDboolEnodes\xa2Hrequired\xf5DtypeDlistDtree\xa2Hrequired\xf5DtypeEbytesKpermissions\x81DpullGpushkey\xa2Dargs\xa4Ckey\xa2Hrequired\xf5DtypeEbytesInamespace\xa2Hrequired\xf5DtypeEbytesCnew\xa2Hrequired\xf5DtypeEbytesCold\xa2Hrequired\xf5DtypeEbytesKpermissions\x81DpushKcompression\x82\xa1DnameDzstd\xa1DnameDzlibQframingmediatypes\x81X&application/mercurial-exp-framing-0005Rpathfilterprefixes\xd9\x01\x02\x82Epath:Lrootfilesin:Nrawrepoformats\x82LgeneraldeltaHrevlogv1Hredirect\xa2Fhashes\x82Fsha256Dsha1Gtargets\x82\xa3DnameHtarget-aHprotocolDhttpDuris\x81Shttp://example.com/\xa3DnameHtarget-bHprotocolGunknownDuris\x81Vunknown://example.com/
377 386 s> \r\n
378 387 received frame(size=1470; request=1; stream=2; streamflags=; type=command-response; flags=continuation)
379 388 s> 8\r\n
380 389 s> \x00\x00\x00\x01\x00\x02\x002
381 390 s> \r\n
382 391 s> 0\r\n
383 392 s> \r\n
384 393 received frame(size=0; request=1; stream=2; streamflags=; type=command-response; flags=eos)
385 394 response: gen[
386 395 {
387 396 b'commands': {
388 397 b'branchmap': {
389 398 b'args': {},
390 399 b'permissions': [
391 400 b'pull'
392 401 ]
393 402 },
394 403 b'capabilities': {
395 404 b'args': {},
396 405 b'permissions': [
397 406 b'pull'
398 407 ]
399 408 },
400 409 b'changesetdata': {
401 410 b'args': {
402 411 b'fields': {
403 412 b'default': set([]),
404 413 b'required': False,
405 414 b'type': b'set',
406 415 b'validvalues': set([
407 416 b'bookmarks',
408 417 b'parents',
409 418 b'phase',
410 419 b'revision'
411 420 ])
412 421 },
413 422 b'noderange': {
414 423 b'default': None,
415 424 b'required': False,
416 425 b'type': b'list'
417 426 },
418 427 b'nodes': {
419 428 b'default': None,
420 429 b'required': False,
421 430 b'type': b'list'
422 431 },
423 432 b'nodesdepth': {
424 433 b'default': None,
425 434 b'required': False,
426 435 b'type': b'int'
427 436 }
428 437 },
429 438 b'permissions': [
430 439 b'pull'
431 440 ]
432 441 },
433 442 b'filedata': {
434 443 b'args': {
435 444 b'fields': {
436 445 b'default': set([]),
437 446 b'required': False,
438 447 b'type': b'set',
439 448 b'validvalues': set([
440 449 b'parents',
441 450 b'revision'
442 451 ])
443 452 },
444 453 b'haveparents': {
445 454 b'default': False,
446 455 b'required': False,
447 456 b'type': b'bool'
448 457 },
449 458 b'nodes': {
450 459 b'required': True,
451 460 b'type': b'list'
452 461 },
453 462 b'path': {
454 463 b'required': True,
455 464 b'type': b'bytes'
456 465 }
457 466 },
458 467 b'permissions': [
459 468 b'pull'
460 469 ]
461 470 },
462 471 b'heads': {
463 472 b'args': {
464 473 b'publiconly': {
465 474 b'default': False,
466 475 b'required': False,
467 476 b'type': b'bool'
468 477 }
469 478 },
470 479 b'permissions': [
471 480 b'pull'
472 481 ]
473 482 },
474 483 b'known': {
475 484 b'args': {
476 485 b'nodes': {
477 486 b'default': [],
478 487 b'required': False,
479 488 b'type': b'list'
480 489 }
481 490 },
482 491 b'permissions': [
483 492 b'pull'
484 493 ]
485 494 },
486 495 b'listkeys': {
487 496 b'args': {
488 497 b'namespace': {
489 498 b'required': True,
490 499 b'type': b'bytes'
491 500 }
492 501 },
493 502 b'permissions': [
494 503 b'pull'
495 504 ]
496 505 },
497 506 b'lookup': {
498 507 b'args': {
499 508 b'key': {
500 509 b'required': True,
501 510 b'type': b'bytes'
502 511 }
503 512 },
504 513 b'permissions': [
505 514 b'pull'
506 515 ]
507 516 },
508 517 b'manifestdata': {
509 518 b'args': {
510 519 b'fields': {
511 520 b'default': set([]),
512 521 b'required': False,
513 522 b'type': b'set',
514 523 b'validvalues': set([
515 524 b'parents',
516 525 b'revision'
517 526 ])
518 527 },
519 528 b'haveparents': {
520 529 b'default': False,
521 530 b'required': False,
522 531 b'type': b'bool'
523 532 },
524 533 b'nodes': {
525 534 b'required': True,
526 535 b'type': b'list'
527 536 },
528 537 b'tree': {
529 538 b'required': True,
530 539 b'type': b'bytes'
531 540 }
532 541 },
533 542 b'permissions': [
534 543 b'pull'
535 544 ]
536 545 },
537 546 b'pushkey': {
538 547 b'args': {
539 548 b'key': {
540 549 b'required': True,
541 550 b'type': b'bytes'
542 551 },
543 552 b'namespace': {
544 553 b'required': True,
545 554 b'type': b'bytes'
546 555 },
547 556 b'new': {
548 557 b'required': True,
549 558 b'type': b'bytes'
550 559 },
551 560 b'old': {
552 561 b'required': True,
553 562 b'type': b'bytes'
554 563 }
555 564 },
556 565 b'permissions': [
557 566 b'push'
558 567 ]
559 568 }
560 569 },
561 570 b'compression': [
562 571 {
563 572 b'name': b'zstd'
564 573 },
565 574 {
566 575 b'name': b'zlib'
567 576 }
568 577 ],
569 578 b'framingmediatypes': [
570 579 b'application/mercurial-exp-framing-0005'
571 580 ],
572 581 b'pathfilterprefixes': set([
573 582 b'path:',
574 583 b'rootfilesin:'
575 584 ]),
576 585 b'rawrepoformats': [
577 586 b'generaldelta',
578 587 b'revlogv1'
579 588 ],
580 589 b'redirect': {
581 590 b'hashes': [
582 591 b'sha256',
583 592 b'sha1'
584 593 ],
585 594 b'targets': [
586 595 {
587 596 b'name': b'target-a',
588 597 b'protocol': b'http',
589 598 b'uris': [
590 599 b'http://example.com/'
591 600 ]
592 601 },
593 602 {
594 603 b'name': b'target-b',
595 604 b'protocol': b'unknown',
596 605 b'uris': [
597 606 b'unknown://example.com/'
598 607 ]
599 608 }
600 609 ]
601 610 }
602 611 }
603 612 ]
604 613
605 614 Missing SNI support filters targets that require SNI
606 615
607 616 $ cat > nosni.py << EOF
608 617 > from mercurial import sslutil
609 618 > sslutil.hassni = False
610 619 > EOF
611 620 $ cat >> $HGRCPATH << EOF
612 621 > [extensions]
613 622 > nosni=`pwd`/nosni.py
614 623 > EOF
615 624
616 625 $ cat > redirects.py << EOF
617 626 > [
618 627 > {
619 628 > b'name': b'target-bad-tls',
620 629 > b'protocol': b'https',
621 630 > b'uris': [b'https://example.com/'],
622 631 > b'snirequired': True,
623 632 > },
624 633 > ]
625 634 > EOF
626 635
627 636 $ sendhttpv2peerhandshake << EOF
628 637 > command capabilities
629 638 > EOF
630 639 creating http peer for wire protocol version 2
631 640 s> GET /?cmd=capabilities HTTP/1.1\r\n
632 641 s> Accept-Encoding: identity\r\n
633 642 s> vary: X-HgProto-1,X-HgUpgrade-1\r\n
634 643 s> x-hgproto-1: cbor\r\n
635 644 s> x-hgupgrade-1: exp-http-v2-0002\r\n
636 645 s> accept: application/mercurial-0.1\r\n
637 646 s> host: $LOCALIP:$HGPORT\r\n (glob)
638 647 s> user-agent: Mercurial debugwireproto\r\n
639 648 s> \r\n
640 649 s> makefile('rb', None)
641 650 s> HTTP/1.1 200 OK\r\n
642 651 s> Server: testing stub value\r\n
643 652 s> Date: $HTTP_DATE$\r\n
644 653 s> Content-Type: application/mercurial-cbor\r\n
645 654 s> Content-Length: 1957\r\n
646 655 s> \r\n
647 656 s> \xa3GapibaseDapi/Dapis\xa1Pexp-http-v2-0002\xa6Hcommands\xaaIbranchmap\xa2Dargs\xa0Kpermissions\x81DpullLcapabilities\xa2Dargs\xa0Kpermissions\x81DpullMchangesetdata\xa2Dargs\xa4Ffields\xa4Gdefault\xd9\x01\x02\x80Hrequired\xf4DtypeCsetKvalidvalues\xd9\x01\x02\x84IbookmarksGparentsEphaseHrevisionInoderange\xa3Gdefault\xf6Hrequired\xf4DtypeDlistEnodes\xa3Gdefault\xf6Hrequired\xf4DtypeDlistJnodesdepth\xa3Gdefault\xf6Hrequired\xf4DtypeCintKpermissions\x81DpullHfiledata\xa2Dargs\xa4Ffields\xa4Gdefault\xd9\x01\x02\x80Hrequired\xf4DtypeCsetKvalidvalues\xd9\x01\x02\x82GparentsHrevisionKhaveparents\xa3Gdefault\xf4Hrequired\xf4DtypeDboolEnodes\xa2Hrequired\xf5DtypeDlistDpath\xa2Hrequired\xf5DtypeEbytesKpermissions\x81DpullEheads\xa2Dargs\xa1Jpubliconly\xa3Gdefault\xf4Hrequired\xf4DtypeDboolKpermissions\x81DpullEknown\xa2Dargs\xa1Enodes\xa3Gdefault\x80Hrequired\xf4DtypeDlistKpermissions\x81DpullHlistkeys\xa2Dargs\xa1Inamespace\xa2Hrequired\xf5DtypeEbytesKpermissions\x81DpullFlookup\xa2Dargs\xa1Ckey\xa2Hrequired\xf5DtypeEbytesKpermissions\x81DpullLmanifestdata\xa2Dargs\xa4Ffields\xa4Gdefault\xd9\x01\x02\x80Hrequired\xf4DtypeCsetKvalidvalues\xd9\x01\x02\x82GparentsHrevisionKhaveparents\xa3Gdefault\xf4Hrequired\xf4DtypeDboolEnodes\xa2Hrequired\xf5DtypeDlistDtree\xa2Hrequired\xf5DtypeEbytesKpermissions\x81DpullGpushkey\xa2Dargs\xa4Ckey\xa2Hrequired\xf5DtypeEbytesInamespace\xa2Hrequired\xf5DtypeEbytesCnew\xa2Hrequired\xf5DtypeEbytesCold\xa2Hrequired\xf5DtypeEbytesKpermissions\x81DpushKcompression\x82\xa1DnameDzstd\xa1DnameDzlibQframingmediatypes\x81X&application/mercurial-exp-framing-0005Rpathfilterprefixes\xd9\x01\x02\x82Epath:Lrootfilesin:Nrawrepoformats\x82LgeneraldeltaHrevlogv1Hredirect\xa2Fhashes\x82Fsha256Dsha1Gtargets\x81\xa4DnameNtarget-bad-tlsHprotocolEhttpsKsnirequired\xf5Duris\x81Thttps://example.com/Nv1capabilitiesY\x01\xd8batch branchmap $USUAL_BUNDLE2_CAPS$ changegroupsubset compression=$BUNDLE2_COMPRESSIONS$ getbundle httpheader=1024 httpmediatype=0.1rx,0.1tx,0.2tx known lookup pushkey streamreqs=generaldelta,revlogv1 unbundle=HG10GZ,HG10BZ,HG10UN unbundlehash
648 657 (redirect target target-bad-tls requires SNI, which is unsupported)
649 658 sending capabilities command
650 659 s> POST /api/exp-http-v2-0002/ro/capabilities HTTP/1.1\r\n
651 660 s> Accept-Encoding: identity\r\n
652 661 s> accept: application/mercurial-exp-framing-0005\r\n
653 662 s> content-type: application/mercurial-exp-framing-0005\r\n
654 663 s> content-length: 66\r\n
655 664 s> host: $LOCALIP:$HGPORT\r\n (glob)
656 665 s> user-agent: Mercurial debugwireproto\r\n
657 666 s> \r\n
658 667 s> :\x00\x00\x01\x00\x01\x01\x11\xa2DnameLcapabilitiesHredirect\xa2Fhashes\x82Fsha256Dsha1Gtargets\x80
659 668 s> makefile('rb', None)
660 669 s> HTTP/1.1 200 OK\r\n
661 670 s> Server: testing stub value\r\n
662 671 s> Date: $HTTP_DATE$\r\n
663 672 s> Content-Type: application/mercurial-exp-framing-0005\r\n
664 673 s> Transfer-Encoding: chunked\r\n
665 674 s> \r\n
666 675 s> 13\r\n
667 676 s> \x0b\x00\x00\x01\x00\x02\x011
668 677 s> \xa1FstatusBok
669 678 s> \r\n
670 679 received frame(size=11; request=1; stream=2; streamflags=stream-begin; type=command-response; flags=continuation)
671 680 s> 59e\r\n
672 681 s> \x96\x05\x00\x01\x00\x02\x001
673 682 s> \xa6Hcommands\xaaIbranchmap\xa2Dargs\xa0Kpermissions\x81DpullLcapabilities\xa2Dargs\xa0Kpermissions\x81DpullMchangesetdata\xa2Dargs\xa4Ffields\xa4Gdefault\xd9\x01\x02\x80Hrequired\xf4DtypeCsetKvalidvalues\xd9\x01\x02\x84IbookmarksGparentsEphaseHrevisionInoderange\xa3Gdefault\xf6Hrequired\xf4DtypeDlistEnodes\xa3Gdefault\xf6Hrequired\xf4DtypeDlistJnodesdepth\xa3Gdefault\xf6Hrequired\xf4DtypeCintKpermissions\x81DpullHfiledata\xa2Dargs\xa4Ffields\xa4Gdefault\xd9\x01\x02\x80Hrequired\xf4DtypeCsetKvalidvalues\xd9\x01\x02\x82GparentsHrevisionKhaveparents\xa3Gdefault\xf4Hrequired\xf4DtypeDboolEnodes\xa2Hrequired\xf5DtypeDlistDpath\xa2Hrequired\xf5DtypeEbytesKpermissions\x81DpullEheads\xa2Dargs\xa1Jpubliconly\xa3Gdefault\xf4Hrequired\xf4DtypeDboolKpermissions\x81DpullEknown\xa2Dargs\xa1Enodes\xa3Gdefault\x80Hrequired\xf4DtypeDlistKpermissions\x81DpullHlistkeys\xa2Dargs\xa1Inamespace\xa2Hrequired\xf5DtypeEbytesKpermissions\x81DpullFlookup\xa2Dargs\xa1Ckey\xa2Hrequired\xf5DtypeEbytesKpermissions\x81DpullLmanifestdata\xa2Dargs\xa4Ffields\xa4Gdefault\xd9\x01\x02\x80Hrequired\xf4DtypeCsetKvalidvalues\xd9\x01\x02\x82GparentsHrevisionKhaveparents\xa3Gdefault\xf4Hrequired\xf4DtypeDboolEnodes\xa2Hrequired\xf5DtypeDlistDtree\xa2Hrequired\xf5DtypeEbytesKpermissions\x81DpullGpushkey\xa2Dargs\xa4Ckey\xa2Hrequired\xf5DtypeEbytesInamespace\xa2Hrequired\xf5DtypeEbytesCnew\xa2Hrequired\xf5DtypeEbytesCold\xa2Hrequired\xf5DtypeEbytesKpermissions\x81DpushKcompression\x82\xa1DnameDzstd\xa1DnameDzlibQframingmediatypes\x81X&application/mercurial-exp-framing-0005Rpathfilterprefixes\xd9\x01\x02\x82Epath:Lrootfilesin:Nrawrepoformats\x82LgeneraldeltaHrevlogv1Hredirect\xa2Fhashes\x82Fsha256Dsha1Gtargets\x81\xa4DnameNtarget-bad-tlsHprotocolEhttpsKsnirequired\xf5Duris\x81Thttps://example.com/
674 683 s> \r\n
675 684 received frame(size=1430; request=1; stream=2; streamflags=; type=command-response; flags=continuation)
676 685 s> 8\r\n
677 686 s> \x00\x00\x00\x01\x00\x02\x002
678 687 s> \r\n
679 688 s> 0\r\n
680 689 s> \r\n
681 690 received frame(size=0; request=1; stream=2; streamflags=; type=command-response; flags=eos)
682 691 response: gen[
683 692 {
684 693 b'commands': {
685 694 b'branchmap': {
686 695 b'args': {},
687 696 b'permissions': [
688 697 b'pull'
689 698 ]
690 699 },
691 700 b'capabilities': {
692 701 b'args': {},
693 702 b'permissions': [
694 703 b'pull'
695 704 ]
696 705 },
697 706 b'changesetdata': {
698 707 b'args': {
699 708 b'fields': {
700 709 b'default': set([]),
701 710 b'required': False,
702 711 b'type': b'set',
703 712 b'validvalues': set([
704 713 b'bookmarks',
705 714 b'parents',
706 715 b'phase',
707 716 b'revision'
708 717 ])
709 718 },
710 719 b'noderange': {
711 720 b'default': None,
712 721 b'required': False,
713 722 b'type': b'list'
714 723 },
715 724 b'nodes': {
716 725 b'default': None,
717 726 b'required': False,
718 727 b'type': b'list'
719 728 },
720 729 b'nodesdepth': {
721 730 b'default': None,
722 731 b'required': False,
723 732 b'type': b'int'
724 733 }
725 734 },
726 735 b'permissions': [
727 736 b'pull'
728 737 ]
729 738 },
730 739 b'filedata': {
731 740 b'args': {
732 741 b'fields': {
733 742 b'default': set([]),
734 743 b'required': False,
735 744 b'type': b'set',
736 745 b'validvalues': set([
737 746 b'parents',
738 747 b'revision'
739 748 ])
740 749 },
741 750 b'haveparents': {
742 751 b'default': False,
743 752 b'required': False,
744 753 b'type': b'bool'
745 754 },
746 755 b'nodes': {
747 756 b'required': True,
748 757 b'type': b'list'
749 758 },
750 759 b'path': {
751 760 b'required': True,
752 761 b'type': b'bytes'
753 762 }
754 763 },
755 764 b'permissions': [
756 765 b'pull'
757 766 ]
758 767 },
759 768 b'heads': {
760 769 b'args': {
761 770 b'publiconly': {
762 771 b'default': False,
763 772 b'required': False,
764 773 b'type': b'bool'
765 774 }
766 775 },
767 776 b'permissions': [
768 777 b'pull'
769 778 ]
770 779 },
771 780 b'known': {
772 781 b'args': {
773 782 b'nodes': {
774 783 b'default': [],
775 784 b'required': False,
776 785 b'type': b'list'
777 786 }
778 787 },
779 788 b'permissions': [
780 789 b'pull'
781 790 ]
782 791 },
783 792 b'listkeys': {
784 793 b'args': {
785 794 b'namespace': {
786 795 b'required': True,
787 796 b'type': b'bytes'
788 797 }
789 798 },
790 799 b'permissions': [
791 800 b'pull'
792 801 ]
793 802 },
794 803 b'lookup': {
795 804 b'args': {
796 805 b'key': {
797 806 b'required': True,
798 807 b'type': b'bytes'
799 808 }
800 809 },
801 810 b'permissions': [
802 811 b'pull'
803 812 ]
804 813 },
805 814 b'manifestdata': {
806 815 b'args': {
807 816 b'fields': {
808 817 b'default': set([]),
809 818 b'required': False,
810 819 b'type': b'set',
811 820 b'validvalues': set([
812 821 b'parents',
813 822 b'revision'
814 823 ])
815 824 },
816 825 b'haveparents': {
817 826 b'default': False,
818 827 b'required': False,
819 828 b'type': b'bool'
820 829 },
821 830 b'nodes': {
822 831 b'required': True,
823 832 b'type': b'list'
824 833 },
825 834 b'tree': {
826 835 b'required': True,
827 836 b'type': b'bytes'
828 837 }
829 838 },
830 839 b'permissions': [
831 840 b'pull'
832 841 ]
833 842 },
834 843 b'pushkey': {
835 844 b'args': {
836 845 b'key': {
837 846 b'required': True,
838 847 b'type': b'bytes'
839 848 },
840 849 b'namespace': {
841 850 b'required': True,
842 851 b'type': b'bytes'
843 852 },
844 853 b'new': {
845 854 b'required': True,
846 855 b'type': b'bytes'
847 856 },
848 857 b'old': {
849 858 b'required': True,
850 859 b'type': b'bytes'
851 860 }
852 861 },
853 862 b'permissions': [
854 863 b'push'
855 864 ]
856 865 }
857 866 },
858 867 b'compression': [
859 868 {
860 869 b'name': b'zstd'
861 870 },
862 871 {
863 872 b'name': b'zlib'
864 873 }
865 874 ],
866 875 b'framingmediatypes': [
867 876 b'application/mercurial-exp-framing-0005'
868 877 ],
869 878 b'pathfilterprefixes': set([
870 879 b'path:',
871 880 b'rootfilesin:'
872 881 ]),
873 882 b'rawrepoformats': [
874 883 b'generaldelta',
875 884 b'revlogv1'
876 885 ],
877 886 b'redirect': {
878 887 b'hashes': [
879 888 b'sha256',
880 889 b'sha1'
881 890 ],
882 891 b'targets': [
883 892 {
884 893 b'name': b'target-bad-tls',
885 894 b'protocol': b'https',
886 895 b'snirequired': True,
887 896 b'uris': [
888 897 b'https://example.com/'
889 898 ]
890 899 }
891 900 ]
892 901 }
893 902 }
894 903 ]
895 904
896 905 $ cat >> $HGRCPATH << EOF
897 906 > [extensions]
898 907 > nosni=!
899 908 > EOF
900 909
901 910 Unknown tls value is filtered from compatible targets
902 911
903 912 $ cat > redirects.py << EOF
904 913 > [
905 914 > {
906 915 > b'name': b'target-bad-tls',
907 916 > b'protocol': b'https',
908 917 > b'uris': [b'https://example.com/'],
909 918 > b'tlsversions': [b'42', b'39'],
910 919 > },
911 920 > ]
912 921 > EOF
913 922
914 923 $ sendhttpv2peerhandshake << EOF
915 924 > command capabilities
916 925 > EOF
917 926 creating http peer for wire protocol version 2
918 927 s> GET /?cmd=capabilities HTTP/1.1\r\n
919 928 s> Accept-Encoding: identity\r\n
920 929 s> vary: X-HgProto-1,X-HgUpgrade-1\r\n
921 930 s> x-hgproto-1: cbor\r\n
922 931 s> x-hgupgrade-1: exp-http-v2-0002\r\n
923 932 s> accept: application/mercurial-0.1\r\n
924 933 s> host: $LOCALIP:$HGPORT\r\n (glob)
925 934 s> user-agent: Mercurial debugwireproto\r\n
926 935 s> \r\n
927 936 s> makefile('rb', None)
928 937 s> HTTP/1.1 200 OK\r\n
929 938 s> Server: testing stub value\r\n
930 939 s> Date: $HTTP_DATE$\r\n
931 940 s> Content-Type: application/mercurial-cbor\r\n
932 941 s> Content-Length: 1963\r\n
933 942 s> \r\n
934 943 s> \xa3GapibaseDapi/Dapis\xa1Pexp-http-v2-0002\xa6Hcommands\xaaIbranchmap\xa2Dargs\xa0Kpermissions\x81DpullLcapabilities\xa2Dargs\xa0Kpermissions\x81DpullMchangesetdata\xa2Dargs\xa4Ffields\xa4Gdefault\xd9\x01\x02\x80Hrequired\xf4DtypeCsetKvalidvalues\xd9\x01\x02\x84IbookmarksGparentsEphaseHrevisionInoderange\xa3Gdefault\xf6Hrequired\xf4DtypeDlistEnodes\xa3Gdefault\xf6Hrequired\xf4DtypeDlistJnodesdepth\xa3Gdefault\xf6Hrequired\xf4DtypeCintKpermissions\x81DpullHfiledata\xa2Dargs\xa4Ffields\xa4Gdefault\xd9\x01\x02\x80Hrequired\xf4DtypeCsetKvalidvalues\xd9\x01\x02\x82GparentsHrevisionKhaveparents\xa3Gdefault\xf4Hrequired\xf4DtypeDboolEnodes\xa2Hrequired\xf5DtypeDlistDpath\xa2Hrequired\xf5DtypeEbytesKpermissions\x81DpullEheads\xa2Dargs\xa1Jpubliconly\xa3Gdefault\xf4Hrequired\xf4DtypeDboolKpermissions\x81DpullEknown\xa2Dargs\xa1Enodes\xa3Gdefault\x80Hrequired\xf4DtypeDlistKpermissions\x81DpullHlistkeys\xa2Dargs\xa1Inamespace\xa2Hrequired\xf5DtypeEbytesKpermissions\x81DpullFlookup\xa2Dargs\xa1Ckey\xa2Hrequired\xf5DtypeEbytesKpermissions\x81DpullLmanifestdata\xa2Dargs\xa4Ffields\xa4Gdefault\xd9\x01\x02\x80Hrequired\xf4DtypeCsetKvalidvalues\xd9\x01\x02\x82GparentsHrevisionKhaveparents\xa3Gdefault\xf4Hrequired\xf4DtypeDboolEnodes\xa2Hrequired\xf5DtypeDlistDtree\xa2Hrequired\xf5DtypeEbytesKpermissions\x81DpullGpushkey\xa2Dargs\xa4Ckey\xa2Hrequired\xf5DtypeEbytesInamespace\xa2Hrequired\xf5DtypeEbytesCnew\xa2Hrequired\xf5DtypeEbytesCold\xa2Hrequired\xf5DtypeEbytesKpermissions\x81DpushKcompression\x82\xa1DnameDzstd\xa1DnameDzlibQframingmediatypes\x81X&application/mercurial-exp-framing-0005Rpathfilterprefixes\xd9\x01\x02\x82Epath:Lrootfilesin:Nrawrepoformats\x82LgeneraldeltaHrevlogv1Hredirect\xa2Fhashes\x82Fsha256Dsha1Gtargets\x81\xa4DnameNtarget-bad-tlsHprotocolEhttpsKtlsversions\x82B42B39Duris\x81Thttps://example.com/Nv1capabilitiesY\x01\xd8batch branchmap $USUAL_BUNDLE2_CAPS$ changegroupsubset compression=$BUNDLE2_COMPRESSIONS$ getbundle httpheader=1024 httpmediatype=0.1rx,0.1tx,0.2tx known lookup pushkey streamreqs=generaldelta,revlogv1 unbundle=HG10GZ,HG10BZ,HG10UN unbundlehash
935 944 (remote redirect target target-bad-tls requires unsupported TLS versions: 39, 42)
936 945 sending capabilities command
937 946 s> POST /api/exp-http-v2-0002/ro/capabilities HTTP/1.1\r\n
938 947 s> Accept-Encoding: identity\r\n
939 948 s> accept: application/mercurial-exp-framing-0005\r\n
940 949 s> content-type: application/mercurial-exp-framing-0005\r\n
941 950 s> content-length: 66\r\n
942 951 s> host: $LOCALIP:$HGPORT\r\n (glob)
943 952 s> user-agent: Mercurial debugwireproto\r\n
944 953 s> \r\n
945 954 s> :\x00\x00\x01\x00\x01\x01\x11\xa2DnameLcapabilitiesHredirect\xa2Fhashes\x82Fsha256Dsha1Gtargets\x80
946 955 s> makefile('rb', None)
947 956 s> HTTP/1.1 200 OK\r\n
948 957 s> Server: testing stub value\r\n
949 958 s> Date: $HTTP_DATE$\r\n
950 959 s> Content-Type: application/mercurial-exp-framing-0005\r\n
951 960 s> Transfer-Encoding: chunked\r\n
952 961 s> \r\n
953 962 s> 13\r\n
954 963 s> \x0b\x00\x00\x01\x00\x02\x011
955 964 s> \xa1FstatusBok
956 965 s> \r\n
957 966 received frame(size=11; request=1; stream=2; streamflags=stream-begin; type=command-response; flags=continuation)
958 967 s> 5a4\r\n
959 968 s> \x9c\x05\x00\x01\x00\x02\x001
960 969 s> \xa6Hcommands\xaaIbranchmap\xa2Dargs\xa0Kpermissions\x81DpullLcapabilities\xa2Dargs\xa0Kpermissions\x81DpullMchangesetdata\xa2Dargs\xa4Ffields\xa4Gdefault\xd9\x01\x02\x80Hrequired\xf4DtypeCsetKvalidvalues\xd9\x01\x02\x84IbookmarksGparentsEphaseHrevisionInoderange\xa3Gdefault\xf6Hrequired\xf4DtypeDlistEnodes\xa3Gdefault\xf6Hrequired\xf4DtypeDlistJnodesdepth\xa3Gdefault\xf6Hrequired\xf4DtypeCintKpermissions\x81DpullHfiledata\xa2Dargs\xa4Ffields\xa4Gdefault\xd9\x01\x02\x80Hrequired\xf4DtypeCsetKvalidvalues\xd9\x01\x02\x82GparentsHrevisionKhaveparents\xa3Gdefault\xf4Hrequired\xf4DtypeDboolEnodes\xa2Hrequired\xf5DtypeDlistDpath\xa2Hrequired\xf5DtypeEbytesKpermissions\x81DpullEheads\xa2Dargs\xa1Jpubliconly\xa3Gdefault\xf4Hrequired\xf4DtypeDboolKpermissions\x81DpullEknown\xa2Dargs\xa1Enodes\xa3Gdefault\x80Hrequired\xf4DtypeDlistKpermissions\x81DpullHlistkeys\xa2Dargs\xa1Inamespace\xa2Hrequired\xf5DtypeEbytesKpermissions\x81DpullFlookup\xa2Dargs\xa1Ckey\xa2Hrequired\xf5DtypeEbytesKpermissions\x81DpullLmanifestdata\xa2Dargs\xa4Ffields\xa4Gdefault\xd9\x01\x02\x80Hrequired\xf4DtypeCsetKvalidvalues\xd9\x01\x02\x82GparentsHrevisionKhaveparents\xa3Gdefault\xf4Hrequired\xf4DtypeDboolEnodes\xa2Hrequired\xf5DtypeDlistDtree\xa2Hrequired\xf5DtypeEbytesKpermissions\x81DpullGpushkey\xa2Dargs\xa4Ckey\xa2Hrequired\xf5DtypeEbytesInamespace\xa2Hrequired\xf5DtypeEbytesCnew\xa2Hrequired\xf5DtypeEbytesCold\xa2Hrequired\xf5DtypeEbytesKpermissions\x81DpushKcompression\x82\xa1DnameDzstd\xa1DnameDzlibQframingmediatypes\x81X&application/mercurial-exp-framing-0005Rpathfilterprefixes\xd9\x01\x02\x82Epath:Lrootfilesin:Nrawrepoformats\x82LgeneraldeltaHrevlogv1Hredirect\xa2Fhashes\x82Fsha256Dsha1Gtargets\x81\xa4DnameNtarget-bad-tlsHprotocolEhttpsKtlsversions\x82B42B39Duris\x81Thttps://example.com/
961 970 s> \r\n
962 971 received frame(size=1436; request=1; stream=2; streamflags=; type=command-response; flags=continuation)
963 972 s> 8\r\n
964 973 s> \x00\x00\x00\x01\x00\x02\x002
965 974 s> \r\n
966 975 s> 0\r\n
967 976 s> \r\n
968 977 received frame(size=0; request=1; stream=2; streamflags=; type=command-response; flags=eos)
969 978 response: gen[
970 979 {
971 980 b'commands': {
972 981 b'branchmap': {
973 982 b'args': {},
974 983 b'permissions': [
975 984 b'pull'
976 985 ]
977 986 },
978 987 b'capabilities': {
979 988 b'args': {},
980 989 b'permissions': [
981 990 b'pull'
982 991 ]
983 992 },
984 993 b'changesetdata': {
985 994 b'args': {
986 995 b'fields': {
987 996 b'default': set([]),
988 997 b'required': False,
989 998 b'type': b'set',
990 999 b'validvalues': set([
991 1000 b'bookmarks',
992 1001 b'parents',
993 1002 b'phase',
994 1003 b'revision'
995 1004 ])
996 1005 },
997 1006 b'noderange': {
998 1007 b'default': None,
999 1008 b'required': False,
1000 1009 b'type': b'list'
1001 1010 },
1002 1011 b'nodes': {
1003 1012 b'default': None,
1004 1013 b'required': False,
1005 1014 b'type': b'list'
1006 1015 },
1007 1016 b'nodesdepth': {
1008 1017 b'default': None,
1009 1018 b'required': False,
1010 1019 b'type': b'int'
1011 1020 }
1012 1021 },
1013 1022 b'permissions': [
1014 1023 b'pull'
1015 1024 ]
1016 1025 },
1017 1026 b'filedata': {
1018 1027 b'args': {
1019 1028 b'fields': {
1020 1029 b'default': set([]),
1021 1030 b'required': False,
1022 1031 b'type': b'set',
1023 1032 b'validvalues': set([
1024 1033 b'parents',
1025 1034 b'revision'
1026 1035 ])
1027 1036 },
1028 1037 b'haveparents': {
1029 1038 b'default': False,
1030 1039 b'required': False,
1031 1040 b'type': b'bool'
1032 1041 },
1033 1042 b'nodes': {
1034 1043 b'required': True,
1035 1044 b'type': b'list'
1036 1045 },
1037 1046 b'path': {
1038 1047 b'required': True,
1039 1048 b'type': b'bytes'
1040 1049 }
1041 1050 },
1042 1051 b'permissions': [
1043 1052 b'pull'
1044 1053 ]
1045 1054 },
1046 1055 b'heads': {
1047 1056 b'args': {
1048 1057 b'publiconly': {
1049 1058 b'default': False,
1050 1059 b'required': False,
1051 1060 b'type': b'bool'
1052 1061 }
1053 1062 },
1054 1063 b'permissions': [
1055 1064 b'pull'
1056 1065 ]
1057 1066 },
1058 1067 b'known': {
1059 1068 b'args': {
1060 1069 b'nodes': {
1061 1070 b'default': [],
1062 1071 b'required': False,
1063 1072 b'type': b'list'
1064 1073 }
1065 1074 },
1066 1075 b'permissions': [
1067 1076 b'pull'
1068 1077 ]
1069 1078 },
1070 1079 b'listkeys': {
1071 1080 b'args': {
1072 1081 b'namespace': {
1073 1082 b'required': True,
1074 1083 b'type': b'bytes'
1075 1084 }
1076 1085 },
1077 1086 b'permissions': [
1078 1087 b'pull'
1079 1088 ]
1080 1089 },
1081 1090 b'lookup': {
1082 1091 b'args': {
1083 1092 b'key': {
1084 1093 b'required': True,
1085 1094 b'type': b'bytes'
1086 1095 }
1087 1096 },
1088 1097 b'permissions': [
1089 1098 b'pull'
1090 1099 ]
1091 1100 },
1092 1101 b'manifestdata': {
1093 1102 b'args': {
1094 1103 b'fields': {
1095 1104 b'default': set([]),
1096 1105 b'required': False,
1097 1106 b'type': b'set',
1098 1107 b'validvalues': set([
1099 1108 b'parents',
1100 1109 b'revision'
1101 1110 ])
1102 1111 },
1103 1112 b'haveparents': {
1104 1113 b'default': False,
1105 1114 b'required': False,
1106 1115 b'type': b'bool'
1107 1116 },
1108 1117 b'nodes': {
1109 1118 b'required': True,
1110 1119 b'type': b'list'
1111 1120 },
1112 1121 b'tree': {
1113 1122 b'required': True,
1114 1123 b'type': b'bytes'
1115 1124 }
1116 1125 },
1117 1126 b'permissions': [
1118 1127 b'pull'
1119 1128 ]
1120 1129 },
1121 1130 b'pushkey': {
1122 1131 b'args': {
1123 1132 b'key': {
1124 1133 b'required': True,
1125 1134 b'type': b'bytes'
1126 1135 },
1127 1136 b'namespace': {
1128 1137 b'required': True,
1129 1138 b'type': b'bytes'
1130 1139 },
1131 1140 b'new': {
1132 1141 b'required': True,
1133 1142 b'type': b'bytes'
1134 1143 },
1135 1144 b'old': {
1136 1145 b'required': True,
1137 1146 b'type': b'bytes'
1138 1147 }
1139 1148 },
1140 1149 b'permissions': [
1141 1150 b'push'
1142 1151 ]
1143 1152 }
1144 1153 },
1145 1154 b'compression': [
1146 1155 {
1147 1156 b'name': b'zstd'
1148 1157 },
1149 1158 {
1150 1159 b'name': b'zlib'
1151 1160 }
1152 1161 ],
1153 1162 b'framingmediatypes': [
1154 1163 b'application/mercurial-exp-framing-0005'
1155 1164 ],
1156 1165 b'pathfilterprefixes': set([
1157 1166 b'path:',
1158 1167 b'rootfilesin:'
1159 1168 ]),
1160 1169 b'rawrepoformats': [
1161 1170 b'generaldelta',
1162 1171 b'revlogv1'
1163 1172 ],
1164 1173 b'redirect': {
1165 1174 b'hashes': [
1166 1175 b'sha256',
1167 1176 b'sha1'
1168 1177 ],
1169 1178 b'targets': [
1170 1179 {
1171 1180 b'name': b'target-bad-tls',
1172 1181 b'protocol': b'https',
1173 1182 b'tlsversions': [
1174 1183 b'42',
1175 1184 b'39'
1176 1185 ],
1177 1186 b'uris': [
1178 1187 b'https://example.com/'
1179 1188 ]
1180 1189 }
1181 1190 ]
1182 1191 }
1183 1192 }
1184 1193 ]
1185 1194
1195 Set up the server to issue content redirects to its built-in API server.
1196
1197 $ cat > redirects.py << EOF
1198 > [
1199 > {
1200 > b'name': b'local',
1201 > b'protocol': b'http',
1202 > b'uris': [b'http://example.com/'],
1203 > },
1204 > ]
1205 > EOF
1206
1207 Request to eventual cache URL should return 404 (validating the cache server works)
1208
1209 $ sendhttpraw << EOF
1210 > httprequest GET api/simplecache/missingkey
1211 > user-agent: test
1212 > EOF
1213 using raw connection to peer
1214 s> GET /api/simplecache/missingkey HTTP/1.1\r\n
1215 s> Accept-Encoding: identity\r\n
1216 s> user-agent: test\r\n
1217 s> host: $LOCALIP:$HGPORT\r\n (glob)
1218 s> \r\n
1219 s> makefile('rb', None)
1220 s> HTTP/1.1 404 Not Found\r\n
1221 s> Server: testing stub value\r\n
1222 s> Date: $HTTP_DATE$\r\n
1223 s> Content-Type: text/plain\r\n
1224 s> Content-Length: 22\r\n
1225 s> \r\n
1226 s> key not found in cache
1227
1228 Send a cacheable request
1229
1230 $ sendhttpv2peer << EOF
1231 > command manifestdata
1232 > nodes eval:[b'\x99\x2f\x47\x79\x02\x9a\x3d\xf8\xd0\x66\x6d\x00\xbb\x92\x4f\x69\x63\x4e\x26\x41']
1233 > tree eval:b''
1234 > fields eval:[b'parents']
1235 > EOF
1236 creating http peer for wire protocol version 2
1237 sending manifestdata command
1238 s> POST /api/exp-http-v2-0002/ro/manifestdata HTTP/1.1\r\n
1239 s> Accept-Encoding: identity\r\n
1240 s> accept: application/mercurial-exp-framing-0005\r\n
1241 s> content-type: application/mercurial-exp-framing-0005\r\n
1242 s> content-length: 128\r\n
1243 s> host: $LOCALIP:$HGPORT\r\n (glob)
1244 s> user-agent: Mercurial debugwireproto\r\n
1245 s> \r\n
1246 s> x\x00\x00\x01\x00\x01\x01\x11\xa3Dargs\xa3Ffields\x81GparentsEnodes\x81T\x99/Gy\x02\x9a=\xf8\xd0fm\x00\xbb\x92OicN&ADtree@DnameLmanifestdataHredirect\xa2Fhashes\x82Fsha256Dsha1Gtargets\x81Elocal
1247 s> makefile('rb', None)
1248 s> HTTP/1.1 200 OK\r\n
1249 s> Server: testing stub value\r\n
1250 s> Date: $HTTP_DATE$\r\n
1251 s> Content-Type: application/mercurial-exp-framing-0005\r\n
1252 s> Transfer-Encoding: chunked\r\n
1253 s> \r\n
1254 s> 13\r\n
1255 s> \x0b\x00\x00\x01\x00\x02\x011
1256 s> \xa1FstatusBok
1257 s> \r\n
1258 received frame(size=11; request=1; stream=2; streamflags=stream-begin; type=command-response; flags=continuation)
1259 s> 63\r\n
1260 s> [\x00\x00\x01\x00\x02\x001
1261 s> \xa1Jtotalitems\x01\xa2DnodeT\x99/Gy\x02\x9a=\xf8\xd0fm\x00\xbb\x92OicN&AGparents\x82T\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00T\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00
1262 s> \r\n
1263 received frame(size=91; request=1; stream=2; streamflags=; type=command-response; flags=continuation)
1264 s> 8\r\n
1265 s> \x00\x00\x00\x01\x00\x02\x002
1266 s> \r\n
1267 s> 0\r\n
1268 s> \r\n
1269 received frame(size=0; request=1; stream=2; streamflags=; type=command-response; flags=eos)
1270 response: gen[
1271 {
1272 b'totalitems': 1
1273 },
1274 {
1275 b'node': b'\x99/Gy\x02\x9a=\xf8\xd0fm\x00\xbb\x92OicN&A',
1276 b'parents': [
1277 b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00',
1278 b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'
1279 ]
1280 }
1281 ]
1282
1283 Cached entry should be available on server
1284
1285 $ sendhttpraw << EOF
1286 > httprequest GET api/simplecache/c045a581599d58608efd3d93d8129841f2af04a0
1287 > user-agent: test
1288 > EOF
1289 using raw connection to peer
1290 s> GET /api/simplecache/c045a581599d58608efd3d93d8129841f2af04a0 HTTP/1.1\r\n
1291 s> Accept-Encoding: identity\r\n
1292 s> user-agent: test\r\n
1293 s> host: $LOCALIP:$HGPORT\r\n (glob)
1294 s> \r\n
1295 s> makefile('rb', None)
1296 s> HTTP/1.1 200 OK\r\n
1297 s> Server: testing stub value\r\n
1298 s> Date: $HTTP_DATE$\r\n
1299 s> Content-Type: application/mercurial-cbor\r\n
1300 s> Content-Length: 91\r\n
1301 s> \r\n
1302 s> \xa1Jtotalitems\x01\xa2DnodeT\x99/Gy\x02\x9a=\xf8\xd0fm\x00\xbb\x92OicN&AGparents\x82T\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00T\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00
1303 cbor> [
1304 {
1305 b'totalitems': 1
1306 },
1307 {
1308 b'node': b'\x99/Gy\x02\x9a=\xf8\xd0fm\x00\xbb\x92OicN&A',
1309 b'parents': [
1310 b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00',
1311 b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'
1312 ]
1313 }
1314 ]
1315
1316 2nd request should result in content redirect response
1317
1318 $ sendhttpv2peer << EOF
1319 > command manifestdata
1320 > nodes eval:[b'\x99\x2f\x47\x79\x02\x9a\x3d\xf8\xd0\x66\x6d\x00\xbb\x92\x4f\x69\x63\x4e\x26\x41']
1321 > tree eval:b''
1322 > fields eval:[b'parents']
1323 > EOF
1324 creating http peer for wire protocol version 2
1325 sending manifestdata command
1326 s> POST /api/exp-http-v2-0002/ro/manifestdata HTTP/1.1\r\n
1327 s> Accept-Encoding: identity\r\n
1328 s> accept: application/mercurial-exp-framing-0005\r\n
1329 s> content-type: application/mercurial-exp-framing-0005\r\n
1330 s> content-length: 128\r\n
1331 s> host: $LOCALIP:$HGPORT\r\n (glob)
1332 s> user-agent: Mercurial debugwireproto\r\n
1333 s> \r\n
1334 s> x\x00\x00\x01\x00\x01\x01\x11\xa3Dargs\xa3Ffields\x81GparentsEnodes\x81T\x99/Gy\x02\x9a=\xf8\xd0fm\x00\xbb\x92OicN&ADtree@DnameLmanifestdataHredirect\xa2Fhashes\x82Fsha256Dsha1Gtargets\x81Elocal
1335 s> makefile('rb', None)
1336 s> HTTP/1.1 200 OK\r\n
1337 s> Server: testing stub value\r\n
1338 s> Date: $HTTP_DATE$\r\n
1339 s> Content-Type: application/mercurial-exp-framing-0005\r\n
1340 s> Transfer-Encoding: chunked\r\n
1341 s> \r\n
1342 s> *\r\n (glob)
1343 s> \x*\x00\x00\x01\x00\x02\x011 (glob)
1344 s> \xa2Hlocation\xa2ImediatypeX\x1aapplication/mercurial-cborCurl*http://*:$HGPORT/api/simplecache/c045a581599d58608efd3d93d8129841f2af04a0FstatusHredirect (glob)
1345 s> \r\n
1346 received frame(size=*; request=1; stream=2; streamflags=stream-begin; type=command-response; flags=continuation) (glob)
1347 s> 8\r\n
1348 s> \x00\x00\x00\x01\x00\x02\x001
1349 s> \r\n
1350 s> 8\r\n
1351 received frame(size=0; request=1; stream=2; streamflags=; type=command-response; flags=continuation)
1352 s> \x00\x00\x00\x01\x00\x02\x002
1353 s> \r\n
1354 s> 0\r\n
1355 s> \r\n
1356 received frame(size=0; request=1; stream=2; streamflags=; type=command-response; flags=eos)
1357 abort: redirect responses not yet supported
1358 [255]
1359
1186 1360 $ cat error.log
1187 1361 $ killdaemons.py
1362
1363 $ cat .hg/blackbox.log
1364 *> cacher constructed for manifestdata (glob)
1365 *> cache miss for c045a581599d58608efd3d93d8129841f2af04a0 (glob)
1366 *> storing cache entry for c045a581599d58608efd3d93d8129841f2af04a0 (glob)
1367 *> cacher constructed for manifestdata (glob)
1368 *> cache hit for c045a581599d58608efd3d93d8129841f2af04a0 (glob)
1369 *> sending content redirect for c045a581599d58608efd3d93d8129841f2af04a0 to http://*:$HGPORT/api/simplecache/c045a581599d58608efd3d93d8129841f2af04a0 (glob)
@@ -1,490 +1,499 b''
1 1 from __future__ import absolute_import, print_function
2 2
3 3 import unittest
4 4
5 5 from mercurial.thirdparty import (
6 6 cbor,
7 7 )
8 8 from mercurial import (
9 9 util,
10 10 wireprotoframing as framing,
11 11 )
12 12
13 13 ffs = framing.makeframefromhumanstring
14 14
15 15 OK = cbor.dumps({b'status': b'ok'})
16 16
17 17 def makereactor(deferoutput=False):
18 18 return framing.serverreactor(deferoutput=deferoutput)
19 19
20 20 def sendframes(reactor, gen):
21 21 """Send a generator of frame bytearray to a reactor.
22 22
23 23 Emits a generator of results from ``onframerecv()`` calls.
24 24 """
25 25 for frame in gen:
26 26 header = framing.parseheader(frame)
27 27 payload = frame[framing.FRAME_HEADER_SIZE:]
28 28 assert len(payload) == header.length
29 29
30 30 yield reactor.onframerecv(framing.frame(header.requestid,
31 31 header.streamid,
32 32 header.streamflags,
33 33 header.typeid,
34 34 header.flags,
35 35 payload))
36 36
37 37 def sendcommandframes(reactor, stream, rid, cmd, args, datafh=None):
38 38 """Generate frames to run a command and send them to a reactor."""
39 39 return sendframes(reactor,
40 40 framing.createcommandframes(stream, rid, cmd, args,
41 41 datafh))
42 42
43 43
44 44 class ServerReactorTests(unittest.TestCase):
45 45 def _sendsingleframe(self, reactor, f):
46 46 results = list(sendframes(reactor, [f]))
47 47 self.assertEqual(len(results), 1)
48 48
49 49 return results[0]
50 50
51 51 def assertaction(self, res, expected):
52 52 self.assertIsInstance(res, tuple)
53 53 self.assertEqual(len(res), 2)
54 54 self.assertIsInstance(res[1], dict)
55 55 self.assertEqual(res[0], expected)
56 56
57 57 def assertframesequal(self, frames, framestrings):
58 58 expected = [ffs(s) for s in framestrings]
59 59 self.assertEqual(list(frames), expected)
60 60
61 61 def test1framecommand(self):
62 62 """Receiving a command in a single frame yields request to run it."""
63 63 reactor = makereactor()
64 64 stream = framing.stream(1)
65 65 results = list(sendcommandframes(reactor, stream, 1, b'mycommand', {}))
66 66 self.assertEqual(len(results), 1)
67 67 self.assertaction(results[0], b'runcommand')
68 68 self.assertEqual(results[0][1], {
69 69 b'requestid': 1,
70 70 b'command': b'mycommand',
71 71 b'args': {},
72 b'redirect': None,
72 73 b'data': None,
73 74 })
74 75
75 76 result = reactor.oninputeof()
76 77 self.assertaction(result, b'noop')
77 78
78 79 def test1argument(self):
79 80 reactor = makereactor()
80 81 stream = framing.stream(1)
81 82 results = list(sendcommandframes(reactor, stream, 41, b'mycommand',
82 83 {b'foo': b'bar'}))
83 84 self.assertEqual(len(results), 1)
84 85 self.assertaction(results[0], b'runcommand')
85 86 self.assertEqual(results[0][1], {
86 87 b'requestid': 41,
87 88 b'command': b'mycommand',
88 89 b'args': {b'foo': b'bar'},
90 b'redirect': None,
89 91 b'data': None,
90 92 })
91 93
92 94 def testmultiarguments(self):
93 95 reactor = makereactor()
94 96 stream = framing.stream(1)
95 97 results = list(sendcommandframes(reactor, stream, 1, b'mycommand',
96 98 {b'foo': b'bar', b'biz': b'baz'}))
97 99 self.assertEqual(len(results), 1)
98 100 self.assertaction(results[0], b'runcommand')
99 101 self.assertEqual(results[0][1], {
100 102 b'requestid': 1,
101 103 b'command': b'mycommand',
102 104 b'args': {b'foo': b'bar', b'biz': b'baz'},
105 b'redirect': None,
103 106 b'data': None,
104 107 })
105 108
106 109 def testsimplecommanddata(self):
107 110 reactor = makereactor()
108 111 stream = framing.stream(1)
109 112 results = list(sendcommandframes(reactor, stream, 1, b'mycommand', {},
110 113 util.bytesio(b'data!')))
111 114 self.assertEqual(len(results), 2)
112 115 self.assertaction(results[0], b'wantframe')
113 116 self.assertaction(results[1], b'runcommand')
114 117 self.assertEqual(results[1][1], {
115 118 b'requestid': 1,
116 119 b'command': b'mycommand',
117 120 b'args': {},
121 b'redirect': None,
118 122 b'data': b'data!',
119 123 })
120 124
121 125 def testmultipledataframes(self):
122 126 frames = [
123 127 ffs(b'1 1 stream-begin command-request new|have-data '
124 128 b"cbor:{b'name': b'mycommand'}"),
125 129 ffs(b'1 1 0 command-data continuation data1'),
126 130 ffs(b'1 1 0 command-data continuation data2'),
127 131 ffs(b'1 1 0 command-data eos data3'),
128 132 ]
129 133
130 134 reactor = makereactor()
131 135 results = list(sendframes(reactor, frames))
132 136 self.assertEqual(len(results), 4)
133 137 for i in range(3):
134 138 self.assertaction(results[i], b'wantframe')
135 139 self.assertaction(results[3], b'runcommand')
136 140 self.assertEqual(results[3][1], {
137 141 b'requestid': 1,
138 142 b'command': b'mycommand',
139 143 b'args': {},
144 b'redirect': None,
140 145 b'data': b'data1data2data3',
141 146 })
142 147
143 148 def testargumentanddata(self):
144 149 frames = [
145 150 ffs(b'1 1 stream-begin command-request new|have-data '
146 151 b"cbor:{b'name': b'command', b'args': {b'key': b'val',"
147 152 b"b'foo': b'bar'}}"),
148 153 ffs(b'1 1 0 command-data continuation value1'),
149 154 ffs(b'1 1 0 command-data eos value2'),
150 155 ]
151 156
152 157 reactor = makereactor()
153 158 results = list(sendframes(reactor, frames))
154 159
155 160 self.assertaction(results[-1], b'runcommand')
156 161 self.assertEqual(results[-1][1], {
157 162 b'requestid': 1,
158 163 b'command': b'command',
159 164 b'args': {
160 165 b'key': b'val',
161 166 b'foo': b'bar',
162 167 },
168 b'redirect': None,
163 169 b'data': b'value1value2',
164 170 })
165 171
166 172 def testnewandcontinuation(self):
167 173 result = self._sendsingleframe(makereactor(),
168 174 ffs(b'1 1 stream-begin command-request new|continuation '))
169 175 self.assertaction(result, b'error')
170 176 self.assertEqual(result[1], {
171 177 b'message': b'received command request frame with both new and '
172 178 b'continuation flags set',
173 179 })
174 180
175 181 def testneithernewnorcontinuation(self):
176 182 result = self._sendsingleframe(makereactor(),
177 183 ffs(b'1 1 stream-begin command-request 0 '))
178 184 self.assertaction(result, b'error')
179 185 self.assertEqual(result[1], {
180 186 b'message': b'received command request frame with neither new nor '
181 187 b'continuation flags set',
182 188 })
183 189
184 190 def testunexpectedcommanddata(self):
185 191 """Command data frame when not running a command is an error."""
186 192 result = self._sendsingleframe(makereactor(),
187 193 ffs(b'1 1 stream-begin command-data 0 ignored'))
188 194 self.assertaction(result, b'error')
189 195 self.assertEqual(result[1], {
190 196 b'message': b'expected command request frame; got 2',
191 197 })
192 198
193 199 def testunexpectedcommanddatareceiving(self):
194 200 """Same as above except the command is receiving."""
195 201 results = list(sendframes(makereactor(), [
196 202 ffs(b'1 1 stream-begin command-request new|more '
197 203 b"cbor:{b'name': b'ignored'}"),
198 204 ffs(b'1 1 0 command-data eos ignored'),
199 205 ]))
200 206
201 207 self.assertaction(results[0], b'wantframe')
202 208 self.assertaction(results[1], b'error')
203 209 self.assertEqual(results[1][1], {
204 210 b'message': b'received command data frame for request that is not '
205 211 b'expecting data: 1',
206 212 })
207 213
208 214 def testconflictingrequestidallowed(self):
209 215 """Multiple fully serviced commands with same request ID is allowed."""
210 216 reactor = makereactor()
211 217 results = []
212 218 outstream = reactor.makeoutputstream()
213 219 results.append(self._sendsingleframe(
214 220 reactor, ffs(b'1 1 stream-begin command-request new '
215 221 b"cbor:{b'name': b'command'}")))
216 222 result = reactor.oncommandresponseready(outstream, 1, b'response1')
217 223 self.assertaction(result, b'sendframes')
218 224 list(result[1][b'framegen'])
219 225 results.append(self._sendsingleframe(
220 226 reactor, ffs(b'1 1 stream-begin command-request new '
221 227 b"cbor:{b'name': b'command'}")))
222 228 result = reactor.oncommandresponseready(outstream, 1, b'response2')
223 229 self.assertaction(result, b'sendframes')
224 230 list(result[1][b'framegen'])
225 231 results.append(self._sendsingleframe(
226 232 reactor, ffs(b'1 1 stream-begin command-request new '
227 233 b"cbor:{b'name': b'command'}")))
228 234 result = reactor.oncommandresponseready(outstream, 1, b'response3')
229 235 self.assertaction(result, b'sendframes')
230 236 list(result[1][b'framegen'])
231 237
232 238 for i in range(3):
233 239 self.assertaction(results[i], b'runcommand')
234 240 self.assertEqual(results[i][1], {
235 241 b'requestid': 1,
236 242 b'command': b'command',
237 243 b'args': {},
244 b'redirect': None,
238 245 b'data': None,
239 246 })
240 247
241 248 def testconflictingrequestid(self):
242 249 """Request ID for new command matching in-flight command is illegal."""
243 250 results = list(sendframes(makereactor(), [
244 251 ffs(b'1 1 stream-begin command-request new|more '
245 252 b"cbor:{b'name': b'command'}"),
246 253 ffs(b'1 1 0 command-request new '
247 254 b"cbor:{b'name': b'command1'}"),
248 255 ]))
249 256
250 257 self.assertaction(results[0], b'wantframe')
251 258 self.assertaction(results[1], b'error')
252 259 self.assertEqual(results[1][1], {
253 260 b'message': b'request with ID 1 already received',
254 261 })
255 262
256 263 def testinterleavedcommands(self):
257 264 cbor1 = cbor.dumps({
258 265 b'name': b'command1',
259 266 b'args': {
260 267 b'foo': b'bar',
261 268 b'key1': b'val',
262 269 }
263 270 }, canonical=True)
264 271 cbor3 = cbor.dumps({
265 272 b'name': b'command3',
266 273 b'args': {
267 274 b'biz': b'baz',
268 275 b'key': b'val',
269 276 },
270 277 }, canonical=True)
271 278
272 279 results = list(sendframes(makereactor(), [
273 280 ffs(b'1 1 stream-begin command-request new|more %s' % cbor1[0:6]),
274 281 ffs(b'3 1 0 command-request new|more %s' % cbor3[0:10]),
275 282 ffs(b'1 1 0 command-request continuation|more %s' % cbor1[6:9]),
276 283 ffs(b'3 1 0 command-request continuation|more %s' % cbor3[10:13]),
277 284 ffs(b'3 1 0 command-request continuation %s' % cbor3[13:]),
278 285 ffs(b'1 1 0 command-request continuation %s' % cbor1[9:]),
279 286 ]))
280 287
281 288 self.assertEqual([t[0] for t in results], [
282 289 b'wantframe',
283 290 b'wantframe',
284 291 b'wantframe',
285 292 b'wantframe',
286 293 b'runcommand',
287 294 b'runcommand',
288 295 ])
289 296
290 297 self.assertEqual(results[4][1], {
291 298 b'requestid': 3,
292 299 b'command': b'command3',
293 300 b'args': {b'biz': b'baz', b'key': b'val'},
301 b'redirect': None,
294 302 b'data': None,
295 303 })
296 304 self.assertEqual(results[5][1], {
297 305 b'requestid': 1,
298 306 b'command': b'command1',
299 307 b'args': {b'foo': b'bar', b'key1': b'val'},
308 b'redirect': None,
300 309 b'data': None,
301 310 })
302 311
303 312 def testmissingcommanddataframe(self):
304 313 # The reactor doesn't currently handle partially received commands.
305 314 # So this test is failing to do anything with request 1.
306 315 frames = [
307 316 ffs(b'1 1 stream-begin command-request new|have-data '
308 317 b"cbor:{b'name': b'command1'}"),
309 318 ffs(b'3 1 0 command-request new '
310 319 b"cbor:{b'name': b'command2'}"),
311 320 ]
312 321 results = list(sendframes(makereactor(), frames))
313 322 self.assertEqual(len(results), 2)
314 323 self.assertaction(results[0], b'wantframe')
315 324 self.assertaction(results[1], b'runcommand')
316 325
317 326 def testmissingcommanddataframeflags(self):
318 327 frames = [
319 328 ffs(b'1 1 stream-begin command-request new|have-data '
320 329 b"cbor:{b'name': b'command1'}"),
321 330 ffs(b'1 1 0 command-data 0 data'),
322 331 ]
323 332 results = list(sendframes(makereactor(), frames))
324 333 self.assertEqual(len(results), 2)
325 334 self.assertaction(results[0], b'wantframe')
326 335 self.assertaction(results[1], b'error')
327 336 self.assertEqual(results[1][1], {
328 337 b'message': b'command data frame without flags',
329 338 })
330 339
331 340 def testframefornonreceivingrequest(self):
332 341 """Receiving a frame for a command that is not receiving is illegal."""
333 342 results = list(sendframes(makereactor(), [
334 343 ffs(b'1 1 stream-begin command-request new '
335 344 b"cbor:{b'name': b'command1'}"),
336 345 ffs(b'3 1 0 command-request new|have-data '
337 346 b"cbor:{b'name': b'command3'}"),
338 347 ffs(b'5 1 0 command-data eos ignored'),
339 348 ]))
340 349 self.assertaction(results[2], b'error')
341 350 self.assertEqual(results[2][1], {
342 351 b'message': b'received frame for request that is not receiving: 5',
343 352 })
344 353
345 354 def testsimpleresponse(self):
346 355 """Bytes response to command sends result frames."""
347 356 reactor = makereactor()
348 357 instream = framing.stream(1)
349 358 list(sendcommandframes(reactor, instream, 1, b'mycommand', {}))
350 359
351 360 outstream = reactor.makeoutputstream()
352 361 result = reactor.oncommandresponseready(outstream, 1, b'response')
353 362 self.assertaction(result, b'sendframes')
354 363 self.assertframesequal(result[1][b'framegen'], [
355 364 b'1 2 stream-begin command-response eos %sresponse' % OK,
356 365 ])
357 366
358 367 def testmultiframeresponse(self):
359 368 """Bytes response spanning multiple frames is handled."""
360 369 first = b'x' * framing.DEFAULT_MAX_FRAME_SIZE
361 370 second = b'y' * 100
362 371
363 372 reactor = makereactor()
364 373 instream = framing.stream(1)
365 374 list(sendcommandframes(reactor, instream, 1, b'mycommand', {}))
366 375
367 376 outstream = reactor.makeoutputstream()
368 377 result = reactor.oncommandresponseready(outstream, 1, first + second)
369 378 self.assertaction(result, b'sendframes')
370 379 self.assertframesequal(result[1][b'framegen'], [
371 380 b'1 2 stream-begin command-response continuation %s' % OK,
372 381 b'1 2 0 command-response continuation %s' % first,
373 382 b'1 2 0 command-response eos %s' % second,
374 383 ])
375 384
376 385 def testservererror(self):
377 386 reactor = makereactor()
378 387 instream = framing.stream(1)
379 388 list(sendcommandframes(reactor, instream, 1, b'mycommand', {}))
380 389
381 390 outstream = reactor.makeoutputstream()
382 391 result = reactor.onservererror(outstream, 1, b'some message')
383 392 self.assertaction(result, b'sendframes')
384 393 self.assertframesequal(result[1][b'framegen'], [
385 394 b"1 2 stream-begin error-response 0 "
386 395 b"cbor:{b'type': b'server', "
387 396 b"b'message': [{b'msg': b'some message'}]}",
388 397 ])
389 398
390 399 def test1commanddeferresponse(self):
391 400 """Responses when in deferred output mode are delayed until EOF."""
392 401 reactor = makereactor(deferoutput=True)
393 402 instream = framing.stream(1)
394 403 results = list(sendcommandframes(reactor, instream, 1, b'mycommand',
395 404 {}))
396 405 self.assertEqual(len(results), 1)
397 406 self.assertaction(results[0], b'runcommand')
398 407
399 408 outstream = reactor.makeoutputstream()
400 409 result = reactor.oncommandresponseready(outstream, 1, b'response')
401 410 self.assertaction(result, b'noop')
402 411 result = reactor.oninputeof()
403 412 self.assertaction(result, b'sendframes')
404 413 self.assertframesequal(result[1][b'framegen'], [
405 414 b'1 2 stream-begin command-response eos %sresponse' % OK,
406 415 ])
407 416
408 417 def testmultiplecommanddeferresponse(self):
409 418 reactor = makereactor(deferoutput=True)
410 419 instream = framing.stream(1)
411 420 list(sendcommandframes(reactor, instream, 1, b'command1', {}))
412 421 list(sendcommandframes(reactor, instream, 3, b'command2', {}))
413 422
414 423 outstream = reactor.makeoutputstream()
415 424 result = reactor.oncommandresponseready(outstream, 1, b'response1')
416 425 self.assertaction(result, b'noop')
417 426 result = reactor.oncommandresponseready(outstream, 3, b'response2')
418 427 self.assertaction(result, b'noop')
419 428 result = reactor.oninputeof()
420 429 self.assertaction(result, b'sendframes')
421 430 self.assertframesequal(result[1][b'framegen'], [
422 431 b'1 2 stream-begin command-response eos %sresponse1' % OK,
423 432 b'3 2 0 command-response eos %sresponse2' % OK,
424 433 ])
425 434
426 435 def testrequestidtracking(self):
427 436 reactor = makereactor(deferoutput=True)
428 437 instream = framing.stream(1)
429 438 list(sendcommandframes(reactor, instream, 1, b'command1', {}))
430 439 list(sendcommandframes(reactor, instream, 3, b'command2', {}))
431 440 list(sendcommandframes(reactor, instream, 5, b'command3', {}))
432 441
433 442 # Register results for commands out of order.
434 443 outstream = reactor.makeoutputstream()
435 444 reactor.oncommandresponseready(outstream, 3, b'response3')
436 445 reactor.oncommandresponseready(outstream, 1, b'response1')
437 446 reactor.oncommandresponseready(outstream, 5, b'response5')
438 447
439 448 result = reactor.oninputeof()
440 449 self.assertaction(result, b'sendframes')
441 450 self.assertframesequal(result[1][b'framegen'], [
442 451 b'3 2 stream-begin command-response eos %sresponse3' % OK,
443 452 b'1 2 0 command-response eos %sresponse1' % OK,
444 453 b'5 2 0 command-response eos %sresponse5' % OK,
445 454 ])
446 455
447 456 def testduplicaterequestonactivecommand(self):
448 457 """Receiving a request ID that matches a request that isn't finished."""
449 458 reactor = makereactor()
450 459 stream = framing.stream(1)
451 460 list(sendcommandframes(reactor, stream, 1, b'command1', {}))
452 461 results = list(sendcommandframes(reactor, stream, 1, b'command1', {}))
453 462
454 463 self.assertaction(results[0], b'error')
455 464 self.assertEqual(results[0][1], {
456 465 b'message': b'request with ID 1 is already active',
457 466 })
458 467
459 468 def testduplicaterequestonactivecommandnosend(self):
460 469 """Same as above but we've registered a response but haven't sent it."""
461 470 reactor = makereactor()
462 471 instream = framing.stream(1)
463 472 list(sendcommandframes(reactor, instream, 1, b'command1', {}))
464 473 outstream = reactor.makeoutputstream()
465 474 reactor.oncommandresponseready(outstream, 1, b'response')
466 475
467 476 # We've registered the response but haven't sent it. From the
468 477 # perspective of the reactor, the command is still active.
469 478
470 479 results = list(sendcommandframes(reactor, instream, 1, b'command1', {}))
471 480 self.assertaction(results[0], b'error')
472 481 self.assertEqual(results[0][1], {
473 482 b'message': b'request with ID 1 is already active',
474 483 })
475 484
476 485 def testduplicaterequestaftersend(self):
477 486 """We can use a duplicate request ID after we've sent the response."""
478 487 reactor = makereactor()
479 488 instream = framing.stream(1)
480 489 list(sendcommandframes(reactor, instream, 1, b'command1', {}))
481 490 outstream = reactor.makeoutputstream()
482 491 res = reactor.oncommandresponseready(outstream, 1, b'response')
483 492 list(res[1][b'framegen'])
484 493
485 494 results = list(sendcommandframes(reactor, instream, 1, b'command1', {}))
486 495 self.assertaction(results[0], b'runcommand')
487 496
488 497 if __name__ == '__main__':
489 498 import silenttestrunner
490 499 silenttestrunner.main(__name__)
@@ -1,118 +1,193 b''
1 1 # wireprotosimplecache.py - Extension providing in-memory wire protocol cache
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 from __future__ import absolute_import
9 9
10 10 from mercurial import (
11 11 extensions,
12 12 registrar,
13 13 repository,
14 14 util,
15 wireprotoserver,
15 16 wireprototypes,
16 17 wireprotov2server,
17 18 )
18 19 from mercurial.utils import (
19 20 interfaceutil,
20 21 stringutil,
21 22 )
22 23
23 24 CACHE = None
24 25
25 26 configtable = {}
26 27 configitem = registrar.configitem(configtable)
27 28
29 configitem('simplecache', 'cacheapi',
30 default=False)
28 31 configitem('simplecache', 'cacheobjects',
29 32 default=False)
30 33 configitem('simplecache', 'redirectsfile',
31 34 default=None)
32 35
36 # API handler that makes cached keys available.
37 def handlecacherequest(rctx, req, res, checkperm, urlparts):
38 if rctx.repo.ui.configbool('simplecache', 'cacheobjects'):
39 res.status = b'500 Internal Server Error'
40 res.setbodybytes(b'cacheobjects not supported for api server')
41 return
42
43 if not urlparts:
44 res.status = b'200 OK'
45 res.headers[b'Content-Type'] = b'text/plain'
46 res.setbodybytes(b'simple cache server')
47 return
48
49 key = b'/'.join(urlparts)
50
51 if key not in CACHE:
52 res.status = b'404 Not Found'
53 res.headers[b'Content-Type'] = b'text/plain'
54 res.setbodybytes(b'key not found in cache')
55 return
56
57 res.status = b'200 OK'
58 res.headers[b'Content-Type'] = b'application/mercurial-cbor'
59 res.setbodybytes(CACHE[key])
60
61 def cachedescriptor(req, repo):
62 return {}
63
64 wireprotoserver.API_HANDLERS[b'simplecache'] = {
65 'config': (b'simplecache', b'cacheapi'),
66 'handler': handlecacherequest,
67 'apidescriptor': cachedescriptor,
68 }
69
33 70 @interfaceutil.implementer(repository.iwireprotocolcommandcacher)
34 71 class memorycacher(object):
35 def __init__(self, ui, command, encodefn):
72 def __init__(self, ui, command, encodefn, redirecttargets, redirecthashes,
73 req):
36 74 self.ui = ui
37 75 self.encodefn = encodefn
76 self.redirecttargets = redirecttargets
77 self.redirecthashes = redirecthashes
78 self.req = req
38 79 self.key = None
39 80 self.cacheobjects = ui.configbool('simplecache', 'cacheobjects')
81 self.cacheapi = ui.configbool('simplecache', 'cacheapi')
40 82 self.buffered = []
41 83
42 84 ui.log('simplecache', 'cacher constructed for %s\n', command)
43 85
44 86 def __enter__(self):
45 87 return self
46 88
47 89 def __exit__(self, exctype, excvalue, exctb):
48 90 if exctype:
49 91 self.ui.log('simplecache', 'cacher exiting due to error\n')
50 92
51 93 def adjustcachekeystate(self, state):
52 94 # Needed in order to make tests deterministic. Don't copy this
53 95 # pattern for production caches!
54 96 del state[b'repo']
55 97
56 98 def setcachekey(self, key):
57 99 self.key = key
58 100 return True
59 101
60 102 def lookup(self):
61 103 if self.key not in CACHE:
62 104 self.ui.log('simplecache', 'cache miss for %s\n', self.key)
63 105 return None
64 106
65 107 entry = CACHE[self.key]
66 108 self.ui.log('simplecache', 'cache hit for %s\n', self.key)
67 109
110 redirectable = True
111
112 if not self.cacheapi:
113 redirectable = False
114 elif not self.redirecttargets:
115 redirectable = False
116 else:
117 clienttargets = set(self.redirecttargets)
118 ourtargets = set(t[b'name'] for t in loadredirecttargets(self.ui))
119
120 # We only ever redirect to a single target (for now). So we don't
121 # need to store which target matched.
122 if not clienttargets & ourtargets:
123 redirectable = False
124
125 if redirectable:
126 paths = self.req.dispatchparts[:-3]
127 paths.append(b'simplecache')
128 paths.append(self.key)
129
130 url = b'%s/%s' % (self.req.advertisedbaseurl, b'/'.join(paths))
131
132 #url = b'http://example.com/%s' % self.key
133 self.ui.log('simplecache', 'sending content redirect for %s to '
134 '%s\n', self.key, url)
135 response = wireprototypes.alternatelocationresponse(
136 url=url,
137 mediatype=b'application/mercurial-cbor')
138
139 return {'objs': [response]}
140
68 141 if self.cacheobjects:
69 142 return {
70 143 'objs': entry,
71 144 }
72 145 else:
73 146 return {
74 147 'objs': [wireprototypes.encodedresponse(entry)],
75 148 }
76 149
77 150 def onobject(self, obj):
78 151 if self.cacheobjects:
79 152 self.buffered.append(obj)
80 153 else:
81 154 self.buffered.extend(self.encodefn(obj))
82 155
83 156 yield obj
84 157
85 158 def onfinished(self):
86 159 self.ui.log('simplecache', 'storing cache entry for %s\n', self.key)
87 160 if self.cacheobjects:
88 161 CACHE[self.key] = self.buffered
89 162 else:
90 163 CACHE[self.key] = b''.join(self.buffered)
91 164
92 165 return []
93 166
94 def makeresponsecacher(orig, repo, proto, command, args, objencoderfn):
95 return memorycacher(repo.ui, command, objencoderfn)
167 def makeresponsecacher(orig, repo, proto, command, args, objencoderfn,
168 redirecttargets, redirecthashes):
169 return memorycacher(repo.ui, command, objencoderfn, redirecttargets,
170 redirecthashes, proto._req)
96 171
97 172 def loadredirecttargets(ui):
98 173 path = ui.config('simplecache', 'redirectsfile')
99 174 if not path:
100 175 return []
101 176
102 177 with open(path, 'rb') as fh:
103 178 s = fh.read()
104 179
105 180 return stringutil.evalpythonliteral(s)
106 181
107 182 def getadvertisedredirecttargets(orig, repo, proto):
108 183 return loadredirecttargets(repo.ui)
109 184
110 185 def extsetup(ui):
111 186 global CACHE
112 187
113 188 CACHE = util.lrucachedict(10000)
114 189
115 190 extensions.wrapfunction(wireprotov2server, 'makeresponsecacher',
116 191 makeresponsecacher)
117 192 extensions.wrapfunction(wireprotov2server, 'getadvertisedredirecttargets',
118 193 getadvertisedredirecttargets)
General Comments 0
You need to be logged in to leave comments. Login now