##// END OF EJS Templates
wireprotov2: change command response protocol to include a leading map...
Gregory Szorc -
r37743:3ea8323d default
parent child Browse files
Show More
@@ -1,1863 +1,1892 b''
1 1 The Mercurial wire protocol is a request-response based protocol
2 2 with multiple wire representations.
3 3
4 4 Each request is modeled as a command name, a dictionary of arguments, and
5 5 optional raw input. Command arguments and their types are intrinsic
6 6 properties of commands. So is the response type of the command. This means
7 7 clients can't always send arbitrary arguments to servers and servers can't
8 8 return multiple response types.
9 9
10 10 The protocol is synchronous and does not support multiplexing (concurrent
11 11 commands).
12 12
13 13 Handshake
14 14 =========
15 15
16 16 It is required or common for clients to perform a *handshake* when connecting
17 17 to a server. The handshake serves the following purposes:
18 18
19 19 * Negotiating protocol/transport level options
20 20 * Allows the client to learn about server capabilities to influence
21 21 future requests
22 22 * Ensures the underlying transport channel is in a *clean* state
23 23
24 24 An important goal of the handshake is to allow clients to use more modern
25 25 wire protocol features. By default, clients must assume they are talking
26 26 to an old version of Mercurial server (possibly even the very first
27 27 implementation). So, clients should not attempt to call or utilize modern
28 28 wire protocol features until they have confirmation that the server
29 29 supports them. The handshake implementation is designed to allow both
30 30 ends to utilize the latest set of features and capabilities with as
31 31 few round trips as possible.
32 32
33 33 The handshake mechanism varies by transport and protocol and is documented
34 34 in the sections below.
35 35
36 36 HTTP Protocol
37 37 =============
38 38
39 39 Handshake
40 40 ---------
41 41
42 42 The client sends a ``capabilities`` command request (``?cmd=capabilities``)
43 43 as soon as HTTP requests may be issued.
44 44
45 45 By default, the server responds with a version 1 capabilities string, which
46 46 the client parses to learn about the server's abilities. The ``Content-Type``
47 47 for this response is ``application/mercurial-0.1`` or
48 48 ``application/mercurial-0.2`` depending on whether the client advertised
49 49 support for version ``0.2`` in its request. (Clients aren't supposed to
50 50 advertise support for ``0.2`` until the capabilities response indicates
51 51 the server's support for that media type. However, a client could
52 52 conceivably cache this metadata and issue the capabilities request in such
53 53 a way to elicit an ``application/mercurial-0.2`` response.)
54 54
55 55 Clients wishing to switch to a newer API service may send an
56 56 ``X-HgUpgrade-<X>`` header containing a space-delimited list of API service
57 57 names the client is capable of speaking. The request MUST also include an
58 58 ``X-HgProto-<X>`` header advertising a known serialization format for the
59 59 response. ``cbor`` is currently the only defined serialization format.
60 60
61 61 If the request contains these headers, the response ``Content-Type`` MAY
62 62 be for a different media type. e.g. ``application/mercurial-cbor`` if the
63 63 client advertises support for CBOR.
64 64
65 65 The response MUST be deserializable to a map with the following keys:
66 66
67 67 apibase
68 68 URL path to API services, relative to the repository root. e.g. ``api/``.
69 69
70 70 apis
71 71 A map of API service names to API descriptors. An API descriptor contains
72 72 more details about that API. In the case of the HTTP Version 2 Transport,
73 73 it will be the normal response to a ``capabilities`` command.
74 74
75 75 Only the services advertised by the client that are also available on
76 76 the server are advertised.
77 77
78 78 v1capabilities
79 79 The capabilities string that would be returned by a version 1 response.
80 80
81 81 The client can then inspect the server-advertised APIs and decide which
82 82 API to use, including continuing to use the HTTP Version 1 Transport.
83 83
84 84 HTTP Version 1 Transport
85 85 ------------------------
86 86
87 87 Commands are issued as HTTP/1.0 or HTTP/1.1 requests. Commands are
88 88 sent to the base URL of the repository with the command name sent in
89 89 the ``cmd`` query string parameter. e.g.
90 90 ``https://example.com/repo?cmd=capabilities``. The HTTP method is ``GET``
91 91 or ``POST`` depending on the command and whether there is a request
92 92 body.
93 93
94 94 Command arguments can be sent multiple ways.
95 95
96 96 The simplest is part of the URL query string using ``x-www-form-urlencoded``
97 97 encoding (see Python's ``urllib.urlencode()``. However, many servers impose
98 98 length limitations on the URL. So this mechanism is typically only used if
99 99 the server doesn't support other mechanisms.
100 100
101 101 If the server supports the ``httpheader`` capability, command arguments can
102 102 be sent in HTTP request headers named ``X-HgArg-<N>`` where ``<N>`` is an
103 103 integer starting at 1. A ``x-www-form-urlencoded`` representation of the
104 104 arguments is obtained. This full string is then split into chunks and sent
105 105 in numbered ``X-HgArg-<N>`` headers. The maximum length of each HTTP header
106 106 is defined by the server in the ``httpheader`` capability value, which defaults
107 107 to ``1024``. The server reassembles the encoded arguments string by
108 108 concatenating the ``X-HgArg-<N>`` headers then URL decodes them into a
109 109 dictionary.
110 110
111 111 The list of ``X-HgArg-<N>`` headers should be added to the ``Vary`` request
112 112 header to instruct caches to take these headers into consideration when caching
113 113 requests.
114 114
115 115 If the server supports the ``httppostargs`` capability, the client
116 116 may send command arguments in the HTTP request body as part of an
117 117 HTTP POST request. The command arguments will be URL encoded just like
118 118 they would for sending them via HTTP headers. However, no splitting is
119 119 performed: the raw arguments are included in the HTTP request body.
120 120
121 121 The client sends a ``X-HgArgs-Post`` header with the string length of the
122 122 encoded arguments data. Additional data may be included in the HTTP
123 123 request body immediately following the argument data. The offset of the
124 124 non-argument data is defined by the ``X-HgArgs-Post`` header. The
125 125 ``X-HgArgs-Post`` header is not required if there is no argument data.
126 126
127 127 Additional command data can be sent as part of the HTTP request body. The
128 128 default ``Content-Type`` when sending data is ``application/mercurial-0.1``.
129 129 A ``Content-Length`` header is currently always sent.
130 130
131 131 Example HTTP requests::
132 132
133 133 GET /repo?cmd=capabilities
134 134 X-HgArg-1: foo=bar&baz=hello%20world
135 135
136 136 The request media type should be chosen based on server support. If the
137 137 ``httpmediatype`` server capability is present, the client should send
138 138 the newest mutually supported media type. If this capability is absent,
139 139 the client must assume the server only supports the
140 140 ``application/mercurial-0.1`` media type.
141 141
142 142 The ``Content-Type`` HTTP response header identifies the response as coming
143 143 from Mercurial and can also be used to signal an error has occurred.
144 144
145 145 The ``application/mercurial-*`` media types indicate a generic Mercurial
146 146 data type.
147 147
148 148 The ``application/mercurial-0.1`` media type is raw Mercurial data. It is the
149 149 predecessor of the format below.
150 150
151 151 The ``application/mercurial-0.2`` media type is compression framed Mercurial
152 152 data. The first byte of the payload indicates the length of the compression
153 153 format identifier that follows. Next are N bytes indicating the compression
154 154 format. e.g. ``zlib``. The remaining bytes are compressed according to that
155 155 compression format. The decompressed data behaves the same as with
156 156 ``application/mercurial-0.1``.
157 157
158 158 The ``application/hg-error`` media type indicates a generic error occurred.
159 159 The content of the HTTP response body typically holds text describing the
160 160 error.
161 161
162 162 The ``application/mercurial-cbor`` media type indicates a CBOR payload
163 163 and should be interpreted as identical to ``application/cbor``.
164 164
165 165 Behavior of media types is further described in the ``Content Negotiation``
166 166 section below.
167 167
168 168 Clients should issue a ``User-Agent`` request header that identifies the client.
169 169 The server should not use the ``User-Agent`` for feature detection.
170 170
171 171 A command returning a ``string`` response issues a
172 172 ``application/mercurial-0.*`` media type and the HTTP response body contains
173 173 the raw string value (after compression decoding, if used). A
174 174 ``Content-Length`` header is typically issued, but not required.
175 175
176 176 A command returning a ``stream`` response issues a
177 177 ``application/mercurial-0.*`` media type and the HTTP response is typically
178 178 using *chunked transfer* (``Transfer-Encoding: chunked``).
179 179
180 180 HTTP Version 2 Transport
181 181 ------------------------
182 182
183 183 **Experimental - feature under active development**
184 184
185 185 Version 2 of the HTTP protocol is exposed under the ``/api/*`` URL space.
186 186 It's final API name is not yet formalized.
187 187
188 188 Commands are triggered by sending HTTP POST requests against URLs of the
189 189 form ``<permission>/<command>``, where ``<permission>`` is ``ro`` or
190 190 ``rw``, meaning read-only and read-write, respectively and ``<command>``
191 191 is a named wire protocol command.
192 192
193 193 Non-POST request methods MUST be rejected by the server with an HTTP
194 194 405 response.
195 195
196 196 Commands that modify repository state in meaningful ways MUST NOT be
197 197 exposed under the ``ro`` URL prefix. All available commands MUST be
198 198 available under the ``rw`` URL prefix.
199 199
200 200 Server adminstrators MAY implement blanket HTTP authentication keyed
201 201 off the URL prefix. For example, a server may require authentication
202 202 for all ``rw/*`` URLs and let unauthenticated requests to ``ro/*``
203 203 URL proceed. A server MAY issue an HTTP 401, 403, or 407 response
204 204 in accordance with RFC 7235. Clients SHOULD recognize the HTTP Basic
205 205 (RFC 7617) and Digest (RFC 7616) authentication schemes. Clients SHOULD
206 206 make an attempt to recognize unknown schemes using the
207 207 ``WWW-Authenticate`` response header on a 401 response, as defined by
208 208 RFC 7235.
209 209
210 210 Read-only commands are accessible under ``rw/*`` URLs so clients can
211 211 signal the intent of the operation very early in the connection
212 212 lifecycle. For example, a ``push`` operation - which consists of
213 213 various read-only commands mixed with at least one read-write command -
214 214 can perform all commands against ``rw/*`` URLs so that any server-side
215 215 authentication requirements are discovered upon attempting the first
216 216 command - not potentially several commands into the exchange. This
217 217 allows clients to fail faster or prompt for credentials as soon as the
218 218 exchange takes place. This provides a better end-user experience.
219 219
220 220 Requests to unknown commands or URLS result in an HTTP 404.
221 221 TODO formally define response type, how error is communicated, etc.
222 222
223 223 HTTP request and response bodies use the *Unified Frame-Based Protocol*
224 224 (defined below) for media exchange. The entirety of the HTTP message
225 225 body is 0 or more frames as defined by this protocol.
226 226
227 227 Clients and servers MUST advertise the ``TBD`` media type via the
228 228 ``Content-Type`` request and response headers. In addition, clients MUST
229 229 advertise this media type value in their ``Accept`` request header in all
230 230 requests.
231 231 TODO finalize the media type. For now, it is defined in wireprotoserver.py.
232 232
233 233 Servers receiving requests without an ``Accept`` header SHOULD respond with
234 234 an HTTP 406.
235 235
236 236 Servers receiving requests with an invalid ``Content-Type`` header SHOULD
237 237 respond with an HTTP 415.
238 238
239 239 The command to run is specified in the POST payload as defined by the
240 240 *Unified Frame-Based Protocol*. This is redundant with data already
241 241 encoded in the URL. This is by design, so server operators can have
242 242 better understanding about server activity from looking merely at
243 243 HTTP access logs.
244 244
245 245 In most circumstances, the command specified in the URL MUST match
246 246 the command specified in the frame-based payload or the server will
247 247 respond with an error. The exception to this is the special
248 248 ``multirequest`` URL. (See below.) In addition, HTTP requests
249 249 are limited to one command invocation. The exception is the special
250 250 ``multirequest`` URL.
251 251
252 252 The ``multirequest`` command endpoints (``ro/multirequest`` and
253 253 ``rw/multirequest``) are special in that they allow the execution of
254 254 *any* command and allow the execution of multiple commands. If the
255 255 HTTP request issues multiple commands across multiple frames, all
256 256 issued commands will be processed by the server. Per the defined
257 257 behavior of the *Unified Frame-Based Protocol*, commands may be
258 258 issued interleaved and responses may come back in a different order
259 259 than they were issued. Clients MUST be able to deal with this.
260 260
261 261 SSH Protocol
262 262 ============
263 263
264 264 Handshake
265 265 ---------
266 266
267 267 For all clients, the handshake consists of the client sending 1 or more
268 268 commands to the server using version 1 of the transport. Servers respond
269 269 to commands they know how to respond to and send an empty response (``0\n``)
270 270 for unknown commands (per standard behavior of version 1 of the transport).
271 271 Clients then typically look for a response to the newest sent command to
272 272 determine which transport version to use and what the available features for
273 273 the connection and server are.
274 274
275 275 Preceding any response from client-issued commands, the server may print
276 276 non-protocol output. It is common for SSH servers to print banners, message
277 277 of the day announcements, etc when clients connect. It is assumed that any
278 278 such *banner* output will precede any Mercurial server output. So clients
279 279 must be prepared to handle server output on initial connect that isn't
280 280 in response to any client-issued command and doesn't conform to Mercurial's
281 281 wire protocol. This *banner* output should only be on stdout. However,
282 282 some servers may send output on stderr.
283 283
284 284 Pre 0.9.1 clients issue a ``between`` command with the ``pairs`` argument
285 285 having the value
286 286 ``0000000000000000000000000000000000000000-0000000000000000000000000000000000000000``.
287 287
288 288 The ``between`` command has been supported since the original Mercurial
289 289 SSH server. Requesting the empty range will return a ``\n`` string response,
290 290 which will be encoded as ``1\n\n`` (value length of ``1`` followed by a newline
291 291 followed by the value, which happens to be a newline).
292 292
293 293 For pre 0.9.1 clients and all servers, the exchange looks like::
294 294
295 295 c: between\n
296 296 c: pairs 81\n
297 297 c: 0000000000000000000000000000000000000000-0000000000000000000000000000000000000000
298 298 s: 1\n
299 299 s: \n
300 300
301 301 0.9.1+ clients send a ``hello`` command (with no arguments) before the
302 302 ``between`` command. The response to this command allows clients to
303 303 discover server capabilities and settings.
304 304
305 305 An example exchange between 0.9.1+ clients and a ``hello`` aware server looks
306 306 like::
307 307
308 308 c: hello\n
309 309 c: between\n
310 310 c: pairs 81\n
311 311 c: 0000000000000000000000000000000000000000-0000000000000000000000000000000000000000
312 312 s: 324\n
313 313 s: capabilities: lookup changegroupsubset branchmap pushkey known getbundle ...\n
314 314 s: 1\n
315 315 s: \n
316 316
317 317 And a similar scenario but with servers sending a banner on connect::
318 318
319 319 c: hello\n
320 320 c: between\n
321 321 c: pairs 81\n
322 322 c: 0000000000000000000000000000000000000000-0000000000000000000000000000000000000000
323 323 s: welcome to the server\n
324 324 s: if you find any issues, email someone@somewhere.com\n
325 325 s: 324\n
326 326 s: capabilities: lookup changegroupsubset branchmap pushkey known getbundle ...\n
327 327 s: 1\n
328 328 s: \n
329 329
330 330 Note that output from the ``hello`` command is terminated by a ``\n``. This is
331 331 part of the response payload and not part of the wire protocol adding a newline
332 332 after responses. In other words, the length of the response contains the
333 333 trailing ``\n``.
334 334
335 335 Clients supporting version 2 of the SSH transport send a line beginning
336 336 with ``upgrade`` before the ``hello`` and ``between`` commands. The line
337 337 (which isn't a well-formed command line because it doesn't consist of a
338 338 single command name) serves to both communicate the client's intent to
339 339 switch to transport version 2 (transports are version 1 by default) as
340 340 well as to advertise the client's transport-level capabilities so the
341 341 server may satisfy that request immediately.
342 342
343 343 The upgrade line has the form:
344 344
345 345 upgrade <token> <transport capabilities>
346 346
347 347 That is the literal string ``upgrade`` followed by a space, followed by
348 348 a randomly generated string, followed by a space, followed by a string
349 349 denoting the client's transport capabilities.
350 350
351 351 The token can be anything. However, a random UUID is recommended. (Use
352 352 of version 4 UUIDs is recommended because version 1 UUIDs can leak the
353 353 client's MAC address.)
354 354
355 355 The transport capabilities string is a URL/percent encoded string
356 356 containing key-value pairs defining the client's transport-level
357 357 capabilities. The following capabilities are defined:
358 358
359 359 proto
360 360 A comma-delimited list of transport protocol versions the client
361 361 supports. e.g. ``ssh-v2``.
362 362
363 363 If the server does not recognize the ``upgrade`` line, it should issue
364 364 an empty response and continue processing the ``hello`` and ``between``
365 365 commands. Here is an example handshake between a version 2 aware client
366 366 and a non version 2 aware server:
367 367
368 368 c: upgrade 2e82ab3f-9ce3-4b4e-8f8c-6fd1c0e9e23a proto=ssh-v2
369 369 c: hello\n
370 370 c: between\n
371 371 c: pairs 81\n
372 372 c: 0000000000000000000000000000000000000000-0000000000000000000000000000000000000000
373 373 s: 0\n
374 374 s: 324\n
375 375 s: capabilities: lookup changegroupsubset branchmap pushkey known getbundle ...\n
376 376 s: 1\n
377 377 s: \n
378 378
379 379 (The initial ``0\n`` line from the server indicates an empty response to
380 380 the unknown ``upgrade ..`` command/line.)
381 381
382 382 If the server recognizes the ``upgrade`` line and is willing to satisfy that
383 383 upgrade request, it replies to with a payload of the following form:
384 384
385 385 upgraded <token> <transport name>\n
386 386
387 387 This line is the literal string ``upgraded``, a space, the token that was
388 388 specified by the client in its ``upgrade ...`` request line, a space, and the
389 389 name of the transport protocol that was chosen by the server. The transport
390 390 name MUST match one of the names the client specified in the ``proto`` field
391 391 of its ``upgrade ...`` request line.
392 392
393 393 If a server issues an ``upgraded`` response, it MUST also read and ignore
394 394 the lines associated with the ``hello`` and ``between`` command requests
395 395 that were issued by the server. It is assumed that the negotiated transport
396 396 will respond with equivalent requested information following the transport
397 397 handshake.
398 398
399 399 All data following the ``\n`` terminating the ``upgraded`` line is the
400 400 domain of the negotiated transport. It is common for the data immediately
401 401 following to contain additional metadata about the state of the transport and
402 402 the server. However, this isn't strictly speaking part of the transport
403 403 handshake and isn't covered by this section.
404 404
405 405 Here is an example handshake between a version 2 aware client and a version
406 406 2 aware server:
407 407
408 408 c: upgrade 2e82ab3f-9ce3-4b4e-8f8c-6fd1c0e9e23a proto=ssh-v2
409 409 c: hello\n
410 410 c: between\n
411 411 c: pairs 81\n
412 412 c: 0000000000000000000000000000000000000000-0000000000000000000000000000000000000000
413 413 s: upgraded 2e82ab3f-9ce3-4b4e-8f8c-6fd1c0e9e23a ssh-v2\n
414 414 s: <additional transport specific data>
415 415
416 416 The client-issued token that is echoed in the response provides a more
417 417 resilient mechanism for differentiating *banner* output from Mercurial
418 418 output. In version 1, properly formatted banner output could get confused
419 419 for Mercurial server output. By submitting a randomly generated token
420 420 that is then present in the response, the client can look for that token
421 421 in response lines and have reasonable certainty that the line did not
422 422 originate from a *banner* message.
423 423
424 424 SSH Version 1 Transport
425 425 -----------------------
426 426
427 427 The SSH transport (version 1) is a custom text-based protocol suitable for
428 428 use over any bi-directional stream transport. It is most commonly used with
429 429 SSH.
430 430
431 431 A SSH transport server can be started with ``hg serve --stdio``. The stdin,
432 432 stderr, and stdout file descriptors of the started process are used to exchange
433 433 data. When Mercurial connects to a remote server over SSH, it actually starts
434 434 a ``hg serve --stdio`` process on the remote server.
435 435
436 436 Commands are issued by sending the command name followed by a trailing newline
437 437 ``\n`` to the server. e.g. ``capabilities\n``.
438 438
439 439 Command arguments are sent in the following format::
440 440
441 441 <argument> <length>\n<value>
442 442
443 443 That is, the argument string name followed by a space followed by the
444 444 integer length of the value (expressed as a string) followed by a newline
445 445 (``\n``) followed by the raw argument value.
446 446
447 447 Dictionary arguments are encoded differently::
448 448
449 449 <argument> <# elements>\n
450 450 <key1> <length1>\n<value1>
451 451 <key2> <length2>\n<value2>
452 452 ...
453 453
454 454 Non-argument data is sent immediately after the final argument value. It is
455 455 encoded in chunks::
456 456
457 457 <length>\n<data>
458 458
459 459 Each command declares a list of supported arguments and their types. If a
460 460 client sends an unknown argument to the server, the server should abort
461 461 immediately. The special argument ``*`` in a command's definition indicates
462 462 that all argument names are allowed.
463 463
464 464 The definition of supported arguments and types is initially made when a
465 465 new command is implemented. The client and server must initially independently
466 466 agree on the arguments and their types. This initial set of arguments can be
467 467 supplemented through the presence of *capabilities* advertised by the server.
468 468
469 469 Each command has a defined expected response type.
470 470
471 471 A ``string`` response type is a length framed value. The response consists of
472 472 the string encoded integer length of a value followed by a newline (``\n``)
473 473 followed by the value. Empty values are allowed (and are represented as
474 474 ``0\n``).
475 475
476 476 A ``stream`` response type consists of raw bytes of data. There is no framing.
477 477
478 478 A generic error response type is also supported. It consists of a an error
479 479 message written to ``stderr`` followed by ``\n-\n``. In addition, ``\n`` is
480 480 written to ``stdout``.
481 481
482 482 If the server receives an unknown command, it will send an empty ``string``
483 483 response.
484 484
485 485 The server terminates if it receives an empty command (a ``\n`` character).
486 486
487 487 If the server announces support for the ``protocaps`` capability, the client
488 488 should issue a ``protocaps`` command after the initial handshake to annonunce
489 489 its own capabilities. The client capabilities are persistent.
490 490
491 491 SSH Version 2 Transport
492 492 -----------------------
493 493
494 494 **Experimental and under development**
495 495
496 496 Version 2 of the SSH transport behaves identically to version 1 of the SSH
497 497 transport with the exception of handshake semantics. See above for how
498 498 version 2 of the SSH transport is negotiated.
499 499
500 500 Immediately following the ``upgraded`` line signaling a switch to version
501 501 2 of the SSH protocol, the server automatically sends additional details
502 502 about the capabilities of the remote server. This has the form:
503 503
504 504 <integer length of value>\n
505 505 capabilities: ...\n
506 506
507 507 e.g.
508 508
509 509 s: upgraded 2e82ab3f-9ce3-4b4e-8f8c-6fd1c0e9e23a ssh-v2\n
510 510 s: 240\n
511 511 s: capabilities: known getbundle batch ...\n
512 512
513 513 Following capabilities advertisement, the peers communicate using version
514 514 1 of the SSH transport.
515 515
516 516 Unified Frame-Based Protocol
517 517 ============================
518 518
519 519 **Experimental and under development**
520 520
521 521 The *Unified Frame-Based Protocol* is a communications protocol between
522 522 Mercurial peers. The protocol aims to be mostly transport agnostic
523 523 (works similarly on HTTP, SSH, etc).
524 524
525 525 To operate the protocol, a bi-directional, half-duplex pipe supporting
526 526 ordered sends and receives is required. That is, each peer has one pipe
527 527 for sending data and another for receiving.
528 528
529 529 All data is read and written in atomic units called *frames*. These
530 530 are conceptually similar to TCP packets. Higher-level functionality
531 531 is built on the exchange and processing of frames.
532 532
533 533 All frames are associated with a *stream*. A *stream* provides a
534 534 unidirectional grouping of frames. Streams facilitate two goals:
535 535 content encoding and parallelism. There is a dedicated section on
536 536 streams below.
537 537
538 538 The protocol is request-response based: the client issues requests to
539 539 the server, which issues replies to those requests. Server-initiated
540 540 messaging is not currently supported, but this specification carves
541 541 out room to implement it.
542 542
543 543 All frames are associated with a numbered request. Frames can thus
544 544 be logically grouped by their request ID.
545 545
546 546 Frames begin with an 8 octet header followed by a variable length
547 547 payload::
548 548
549 549 +------------------------------------------------+
550 550 | Length (24) |
551 551 +--------------------------------+---------------+
552 552 | Request ID (16) | Stream ID (8) |
553 553 +------------------+-------------+---------------+
554 554 | Stream Flags (8) |
555 555 +-----------+------+
556 556 | Type (4) |
557 557 +-----------+
558 558 | Flags (4) |
559 559 +===========+===================================================|
560 560 | Frame Payload (0...) ...
561 561 +---------------------------------------------------------------+
562 562
563 563 The length of the frame payload is expressed as an unsigned 24 bit
564 564 little endian integer. Values larger than 65535 MUST NOT be used unless
565 565 given permission by the server as part of the negotiated capabilities
566 566 during the handshake. The frame header is not part of the advertised
567 567 frame length. The payload length is the over-the-wire length. If there
568 568 is content encoding applied to the payload as part of the frame's stream,
569 569 the length is the output of that content encoding, not the input.
570 570
571 571 The 16-bit ``Request ID`` field denotes the integer request identifier,
572 572 stored as an unsigned little endian integer. Odd numbered requests are
573 573 client-initiated. Even numbered requests are server-initiated. This
574 574 refers to where the *request* was initiated - not where the *frame* was
575 575 initiated, so servers will send frames with odd ``Request ID`` in
576 576 response to client-initiated requests. Implementations are advised to
577 577 start ordering request identifiers at ``1`` and ``0``, increment by
578 578 ``2``, and wrap around if all available numbers have been exhausted.
579 579
580 580 The 8-bit ``Stream ID`` field denotes the stream that the frame is
581 581 associated with. Frames belonging to a stream may have content
582 582 encoding applied and the receiver may need to decode the raw frame
583 583 payload to obtain the original data. Odd numbered IDs are
584 584 client-initiated. Even numbered IDs are server-initiated.
585 585
586 586 The 8-bit ``Stream Flags`` field defines stream processing semantics.
587 587 See the section on streams below.
588 588
589 589 The 4-bit ``Type`` field denotes the type of frame being sent.
590 590
591 591 The 4-bit ``Flags`` field defines special, per-type attributes for
592 592 the frame.
593 593
594 594 The sections below define the frame types and their behavior.
595 595
596 596 Command Request (``0x01``)
597 597 --------------------------
598 598
599 599 This frame contains a request to run a command.
600 600
601 601 The payload consists of a CBOR map defining the command request. The
602 602 bytestring keys of that map are:
603 603
604 604 name
605 605 Name of the command that should be executed (bytestring).
606 606 args
607 607 Map of bytestring keys to various value types containing the named
608 608 arguments to this command.
609 609
610 610 Each command defines its own set of argument names and their expected
611 611 types.
612 612
613 613 This frame type MUST ONLY be sent from clients to servers: it is illegal
614 614 for a server to send this frame to a client.
615 615
616 616 The following flag values are defined for this type:
617 617
618 618 0x01
619 619 New command request. When set, this frame represents the beginning
620 620 of a new request to run a command. The ``Request ID`` attached to this
621 621 frame MUST NOT be active.
622 622 0x02
623 623 Command request continuation. When set, this frame is a continuation
624 624 from a previous command request frame for its ``Request ID``. This
625 625 flag is set when the CBOR data for a command request does not fit
626 626 in a single frame.
627 627 0x04
628 628 Additional frames expected. When set, the command request didn't fit
629 629 into a single frame and additional CBOR data follows in a subsequent
630 630 frame.
631 631 0x08
632 632 Command data frames expected. When set, command data frames are
633 633 expected to follow the final command request frame for this request.
634 634
635 635 ``0x01`` MUST be set on the initial command request frame for a
636 636 ``Request ID``.
637 637
638 638 ``0x01`` or ``0x02`` MUST be set to indicate this frame's role in
639 639 a series of command request frames.
640 640
641 641 If command data frames are to be sent, ``0x08`` MUST be set on ALL
642 642 command request frames.
643 643
644 644 Command Data (``0x02``)
645 645 -----------------------
646 646
647 647 This frame contains raw data for a command.
648 648
649 649 Most commands can be executed by specifying arguments. However,
650 650 arguments have an upper bound to their length. For commands that
651 651 accept data that is beyond this length or whose length isn't known
652 652 when the command is initially sent, they will need to stream
653 653 arbitrary data to the server. This frame type facilitates the sending
654 654 of this data.
655 655
656 656 The payload of this frame type consists of a stream of raw data to be
657 657 consumed by the command handler on the server. The format of the data
658 658 is command specific.
659 659
660 660 The following flag values are defined for this type:
661 661
662 662 0x01
663 663 Command data continuation. When set, the data for this command
664 664 continues into a subsequent frame.
665 665
666 666 0x02
667 667 End of data. When set, command data has been fully sent to the
668 668 server. The command has been fully issued and no new data for this
669 669 command will be sent. The next frame will belong to a new command.
670 670
671 671 Command Response Data (``0x03``)
672 672 --------------------------------
673 673
674 674 This frame contains response data to an issued command.
675 675
676 Response data ALWAYS consists of a series of 0 or more CBOR encoded
676 Response data ALWAYS consists of a series of 1 or more CBOR encoded
677 677 values. A CBOR value may be using indefinite length encoding. And the
678 678 bytes constituting the value may span several frames.
679 679
680 680 The following flag values are defined for this type:
681 681
682 682 0x01
683 683 Data continuation. When set, an additional frame containing response data
684 684 will follow.
685 685 0x02
686 686 End of data. When set, the response data has been fully sent and
687 687 no additional frames for this response will be sent.
688 688
689 689 The ``0x01`` flag is mutually exclusive with the ``0x02`` flag.
690 690
691 691 Error Response (``0x05``)
692 692 -------------------------
693 693
694 694 An error occurred when processing a request. This could indicate
695 695 a protocol-level failure or an application level failure depending
696 696 on the flags for this message type.
697 697
698 698 The payload for this type is an error message that should be
699 699 displayed to the user.
700 700
701 701 The following flag values are defined for this type:
702 702
703 703 0x01
704 704 The error occurred at the transport/protocol level. If set, the
705 705 connection should be closed.
706 706 0x02
707 707 The error occurred at the application level. e.g. invalid command.
708 708
709 709 Human Output Side-Channel (``0x06``)
710 710 ------------------------------------
711 711
712 712 This frame contains a message that is intended to be displayed to
713 713 people. Whereas most frames communicate machine readable data, this
714 714 frame communicates textual data that is intended to be shown to
715 715 humans.
716 716
717 717 The frame consists of a series of *formatting requests*. Each formatting
718 718 request consists of a formatting string, arguments for that formatting
719 719 string, and labels to apply to that formatting string.
720 720
721 721 A formatting string is a printf()-like string that allows variable
722 722 substitution within the string. Labels allow the rendered text to be
723 723 *decorated*. Assuming use of the canonical Mercurial code base, a
724 724 formatting string can be the input to the ``i18n._`` function. This
725 725 allows messages emitted from the server to be localized. So even if
726 726 the server has different i18n settings, people could see messages in
727 727 their *native* settings. Similarly, the use of labels allows
728 728 decorations like coloring and underlining to be applied using the
729 729 client's configured rendering settings.
730 730
731 731 Formatting strings are similar to ``printf()`` strings or how
732 732 Python's ``%`` operator works. The only supported formatting sequences
733 733 are ``%s`` and ``%%``. ``%s`` will be replaced by whatever the string
734 734 at that position resolves to. ``%%`` will be replaced by ``%``. All
735 735 other 2-byte sequences beginning with ``%`` represent a literal
736 736 ``%`` followed by that character. However, future versions of the
737 737 wire protocol reserve the right to allow clients to opt in to receiving
738 738 formatting strings with additional formatters, hence why ``%%`` is
739 739 required to represent the literal ``%``.
740 740
741 741 The frame payload consists of a CBOR array of CBOR maps. Each map
742 742 defines an *atom* of text data to print. Each *atom* has the following
743 743 bytestring keys:
744 744
745 745 msg
746 746 (bytestring) The formatting string. Content MUST be ASCII.
747 747 args (optional)
748 748 Array of bytestrings defining arguments to the formatting string.
749 749 labels (optional)
750 750 Array of bytestrings defining labels to apply to this atom.
751 751
752 752 All data to be printed MUST be encoded into a single frame: this frame
753 753 does not support spanning data across multiple frames.
754 754
755 755 All textual data encoded in these frames is assumed to be line delimited.
756 756 The last atom in the frame SHOULD end with a newline (``\n``). If it
757 757 doesn't, clients MAY add a newline to facilitate immediate printing.
758 758
759 759 Progress Update (``0x07``)
760 760 --------------------------
761 761
762 762 This frame holds the progress of an operation on the peer. Consumption
763 763 of these frames allows clients to display progress bars, estimated
764 764 completion times, etc.
765 765
766 766 Each frame defines the progress of a single operation on the peer. The
767 767 payload consists of a CBOR map with the following bytestring keys:
768 768
769 769 topic
770 770 Topic name (string)
771 771 pos
772 772 Current numeric position within the topic (integer)
773 773 total
774 774 Total/end numeric position of this topic (unsigned integer)
775 775 label (optional)
776 776 Unit label (string)
777 777 item (optional)
778 778 Item name (string)
779 779
780 780 Progress state is created when a frame is received referencing a
781 781 *topic* that isn't currently tracked. Progress tracking for that
782 782 *topic* is finished when a frame is received reporting the current
783 783 position of that topic as ``-1``.
784 784
785 785 Multiple *topics* may be active at any given time.
786 786
787 787 Rendering of progress information is not mandated or governed by this
788 788 specification: implementations MAY render progress information however
789 789 they see fit, including not at all.
790 790
791 791 The string data describing the topic SHOULD be static strings to
792 792 facilitate receivers localizing that string data. The emitter
793 793 MUST normalize all string data to valid UTF-8 and receivers SHOULD
794 794 validate that received data conforms to UTF-8. The topic name
795 795 SHOULD be ASCII.
796 796
797 797 Stream Encoding Settings (``0x08``)
798 798 -----------------------------------
799 799
800 800 This frame type holds information defining the content encoding
801 801 settings for a *stream*.
802 802
803 803 This frame type is likely consumed by the protocol layer and is not
804 804 passed on to applications.
805 805
806 806 This frame type MUST ONLY occur on frames having the *Beginning of Stream*
807 807 ``Stream Flag`` set.
808 808
809 809 The payload of this frame defines what content encoding has (possibly)
810 810 been applied to the payloads of subsequent frames in this stream.
811 811
812 812 The payload begins with an 8-bit integer defining the length of the
813 813 encoding *profile*, followed by the string name of that profile, which
814 814 must be an ASCII string. All bytes that follow can be used by that
815 815 profile for supplemental settings definitions. See the section below
816 816 on defined encoding profiles.
817 817
818 818 Stream States and Flags
819 819 -----------------------
820 820
821 821 Streams can be in two states: *open* and *closed*. An *open* stream
822 822 is active and frames attached to that stream could arrive at any time.
823 823 A *closed* stream is not active. If a frame attached to a *closed*
824 824 stream arrives, that frame MUST have an appropriate stream flag
825 825 set indicating beginning of stream. All streams are in the *closed*
826 826 state by default.
827 827
828 828 The ``Stream Flags`` field denotes a set of bit flags for defining
829 829 the relationship of this frame within a stream. The following flags
830 830 are defined:
831 831
832 832 0x01
833 833 Beginning of stream. The first frame in the stream MUST set this
834 834 flag. When received, the ``Stream ID`` this frame is attached to
835 835 becomes ``open``.
836 836
837 837 0x02
838 838 End of stream. The last frame in a stream MUST set this flag. When
839 839 received, the ``Stream ID`` this frame is attached to becomes
840 840 ``closed``. Any content encoding context associated with this stream
841 841 can be destroyed after processing the payload of this frame.
842 842
843 843 0x04
844 844 Apply content encoding. When set, any content encoding settings
845 845 defined by the stream should be applied when attempting to read
846 846 the frame. When not set, the frame payload isn't encoded.
847 847
848 848 Streams
849 849 -------
850 850
851 851 Streams - along with ``Request IDs`` - facilitate grouping of frames.
852 852 But the purpose of each is quite different and the groupings they
853 853 constitute are independent.
854 854
855 855 A ``Request ID`` is essentially a tag. It tells you which logical
856 856 request a frame is associated with.
857 857
858 858 A *stream* is a sequence of frames grouped for the express purpose
859 859 of applying a stateful encoding or for denoting sub-groups of frames.
860 860
861 861 Unlike ``Request ID``s which span the request and response, a stream
862 862 is unidirectional and stream IDs are independent from client to
863 863 server.
864 864
865 865 There is no strict hierarchical relationship between ``Request IDs``
866 866 and *streams*. A stream can contain frames having multiple
867 867 ``Request IDs``. Frames belonging to the same ``Request ID`` can
868 868 span multiple streams.
869 869
870 870 One goal of streams is to facilitate content encoding. A stream can
871 871 define an encoding to be applied to frame payloads. For example, the
872 872 payload transmitted over the wire may contain output from a
873 873 zstandard compression operation and the receiving end may decompress
874 874 that payload to obtain the original data.
875 875
876 876 The other goal of streams is to facilitate concurrent execution. For
877 877 example, a server could spawn 4 threads to service a request that can
878 878 be easily parallelized. Each of those 4 threads could write into its
879 879 own stream. Those streams could then in turn be delivered to 4 threads
880 880 on the receiving end, with each thread consuming its stream in near
881 881 isolation. The *main* thread on both ends merely does I/O and
882 882 encodes/decodes frame headers: the bulk of the work is done by worker
883 883 threads.
884 884
885 885 In addition, since content encoding is defined per stream, each
886 886 *worker thread* could perform potentially CPU bound work concurrently
887 887 with other threads. This approach of applying encoding at the
888 888 sub-protocol / stream level eliminates a potential resource constraint
889 889 on the protocol stream as a whole (it is common for the throughput of
890 890 a compression engine to be smaller than the throughput of a network).
891 891
892 892 Having multiple streams - each with their own encoding settings - also
893 893 facilitates the use of advanced data compression techniques. For
894 894 example, a transmitter could see that it is generating data faster
895 895 and slower than the receiving end is consuming it and adjust its
896 896 compression settings to trade CPU for compression ratio accordingly.
897 897
898 898 While streams can define a content encoding, not all frames within
899 899 that stream must use that content encoding. This can be useful when
900 900 data is being served from caches and being derived dynamically. A
901 901 cache could pre-compressed data so the server doesn't have to
902 902 recompress it. The ability to pick and choose which frames are
903 903 compressed allows servers to easily send data to the wire without
904 904 involving potentially expensive encoding overhead.
905 905
906 906 Content Encoding Profiles
907 907 -------------------------
908 908
909 909 Streams can have named content encoding *profiles* associated with
910 910 them. A profile defines a shared understanding of content encoding
911 911 settings and behavior.
912 912
913 913 The following profiles are defined:
914 914
915 915 TBD
916 916
917 Issuing Commands
917 Command Protocol
918 918 ----------------
919 919
920 920 A client can request that a remote run a command by sending it
921 921 frames defining that command. This logical stream is composed of
922 922 1 or more ``Command Request`` frames and and 0 or more ``Command Data``
923 923 frames.
924 924
925 925 All frames composing a single command request MUST be associated with
926 926 the same ``Request ID``.
927 927
928 928 Clients MAY send additional command requests without waiting on the
929 929 response to a previous command request. If they do so, they MUST ensure
930 930 that the ``Request ID`` field of outbound frames does not conflict
931 931 with that of an active ``Request ID`` whose response has not yet been
932 932 fully received.
933 933
934 934 Servers MAY respond to commands in a different order than they were
935 935 sent over the wire. Clients MUST be prepared to deal with this. Servers
936 936 also MAY start executing commands in a different order than they were
937 937 received, or MAY execute multiple commands concurrently.
938 938
939 939 If there is a dependency between commands or a race condition between
940 940 commands executing (e.g. a read-only command that depends on the results
941 941 of a command that mutates the repository), then clients MUST NOT send
942 942 frames issuing a command until a response to all dependent commands has
943 943 been received.
944 944 TODO think about whether we should express dependencies between commands
945 945 to avoid roundtrip latency.
946 946
947 947 A command is defined by a command name, 0 or more command arguments,
948 948 and optional command data.
949 949
950 950 Arguments are the recommended mechanism for transferring fixed sets of
951 951 parameters to a command. Data is appropriate for transferring variable
952 952 data. Thinking in terms of HTTP, arguments would be headers and data
953 953 would be the message body.
954 954
955 955 It is recommended for servers to delay the dispatch of a command
956 956 until all argument have been received. Servers MAY impose limits on the
957 957 maximum argument size.
958 958 TODO define failure mechanism.
959 959
960 960 Servers MAY dispatch to commands immediately once argument data
961 961 is available or delay until command data is received in full.
962 962
963 Once a ``Command Request`` frame is sent, a client must be prepared to
964 receive any of the following frames associated with that request:
965 ``Command Response``, ``Error Response``, ``Human Output Side-Channel``,
966 ``Progress Update``.
967
968 The *main* response for a command will be in ``Command Response`` frames.
969 The payloads of these frames consist of 1 or more CBOR encoded values.
970 The first CBOR value on the first ``Command Response`` frame is special
971 and denotes the overall status of the command. This CBOR map contains
972 the following bytestring keys:
973
974 status
975 (bytestring) A well-defined message containing the overall status of
976 this command request. The following values are defined:
977
978 ok
979 The command was received successfully and its response follows.
980 error
981 There was an error processing the command. More details about the
982 error are encoded in the ``error`` key.
983
984 error (optional)
985 A map containing information about an encountered error. The map has the
986 following keys:
987
988 message
989 (array of maps) A message describing the error. The message uses the
990 same format as those in the ``Human Output Side-Channel`` frame.
991
963 992 Capabilities
964 993 ============
965 994
966 995 Servers advertise supported wire protocol features. This allows clients to
967 996 probe for server features before blindly calling a command or passing a
968 997 specific argument.
969 998
970 999 The server's features are exposed via a *capabilities* string. This is a
971 1000 space-delimited string of tokens/features. Some features are single words
972 1001 like ``lookup`` or ``batch``. Others are complicated key-value pairs
973 1002 advertising sub-features. e.g. ``httpheader=2048``. When complex, non-word
974 1003 values are used, each feature name can define its own encoding of sub-values.
975 1004 Comma-delimited and ``x-www-form-urlencoded`` values are common.
976 1005
977 1006 The following document capabilities defined by the canonical Mercurial server
978 1007 implementation.
979 1008
980 1009 batch
981 1010 -----
982 1011
983 1012 Whether the server supports the ``batch`` command.
984 1013
985 1014 This capability/command was introduced in Mercurial 1.9 (released July 2011).
986 1015
987 1016 branchmap
988 1017 ---------
989 1018
990 1019 Whether the server supports the ``branchmap`` command.
991 1020
992 1021 This capability/command was introduced in Mercurial 1.3 (released July 2009).
993 1022
994 1023 bundle2-exp
995 1024 -----------
996 1025
997 1026 Precursor to ``bundle2`` capability that was used before bundle2 was a
998 1027 stable feature.
999 1028
1000 1029 This capability was introduced in Mercurial 3.0 behind an experimental
1001 1030 flag. This capability should not be observed in the wild.
1002 1031
1003 1032 bundle2
1004 1033 -------
1005 1034
1006 1035 Indicates whether the server supports the ``bundle2`` data exchange format.
1007 1036
1008 1037 The value of the capability is a URL quoted, newline (``\n``) delimited
1009 1038 list of keys or key-value pairs.
1010 1039
1011 1040 A key is simply a URL encoded string.
1012 1041
1013 1042 A key-value pair is a URL encoded key separated from a URL encoded value by
1014 1043 an ``=``. If the value is a list, elements are delimited by a ``,`` after
1015 1044 URL encoding.
1016 1045
1017 1046 For example, say we have the values::
1018 1047
1019 1048 {'HG20': [], 'changegroup': ['01', '02'], 'digests': ['sha1', 'sha512']}
1020 1049
1021 1050 We would first construct a string::
1022 1051
1023 1052 HG20\nchangegroup=01,02\ndigests=sha1,sha512
1024 1053
1025 1054 We would then URL quote this string::
1026 1055
1027 1056 HG20%0Achangegroup%3D01%2C02%0Adigests%3Dsha1%2Csha512
1028 1057
1029 1058 This capability was introduced in Mercurial 3.4 (released May 2015).
1030 1059
1031 1060 changegroupsubset
1032 1061 -----------------
1033 1062
1034 1063 Whether the server supports the ``changegroupsubset`` command.
1035 1064
1036 1065 This capability was introduced in Mercurial 0.9.2 (released December
1037 1066 2006).
1038 1067
1039 1068 This capability was introduced at the same time as the ``lookup``
1040 1069 capability/command.
1041 1070
1042 1071 compression
1043 1072 -----------
1044 1073
1045 1074 Declares support for negotiating compression formats.
1046 1075
1047 1076 Presence of this capability indicates the server supports dynamic selection
1048 1077 of compression formats based on the client request.
1049 1078
1050 1079 Servers advertising this capability are required to support the
1051 1080 ``application/mercurial-0.2`` media type in response to commands returning
1052 1081 streams. Servers may support this media type on any command.
1053 1082
1054 1083 The value of the capability is a comma-delimited list of strings declaring
1055 1084 supported compression formats. The order of the compression formats is in
1056 1085 server-preferred order, most preferred first.
1057 1086
1058 1087 The identifiers used by the official Mercurial distribution are:
1059 1088
1060 1089 bzip2
1061 1090 bzip2
1062 1091 none
1063 1092 uncompressed / raw data
1064 1093 zlib
1065 1094 zlib (no gzip header)
1066 1095 zstd
1067 1096 zstd
1068 1097
1069 1098 This capability was introduced in Mercurial 4.1 (released February 2017).
1070 1099
1071 1100 getbundle
1072 1101 ---------
1073 1102
1074 1103 Whether the server supports the ``getbundle`` command.
1075 1104
1076 1105 This capability was introduced in Mercurial 1.9 (released July 2011).
1077 1106
1078 1107 httpheader
1079 1108 ----------
1080 1109
1081 1110 Whether the server supports receiving command arguments via HTTP request
1082 1111 headers.
1083 1112
1084 1113 The value of the capability is an integer describing the max header
1085 1114 length that clients should send. Clients should ignore any content after a
1086 1115 comma in the value, as this is reserved for future use.
1087 1116
1088 1117 This capability was introduced in Mercurial 1.9 (released July 2011).
1089 1118
1090 1119 httpmediatype
1091 1120 -------------
1092 1121
1093 1122 Indicates which HTTP media types (``Content-Type`` header) the server is
1094 1123 capable of receiving and sending.
1095 1124
1096 1125 The value of the capability is a comma-delimited list of strings identifying
1097 1126 support for media type and transmission direction. The following strings may
1098 1127 be present:
1099 1128
1100 1129 0.1rx
1101 1130 Indicates server support for receiving ``application/mercurial-0.1`` media
1102 1131 types.
1103 1132
1104 1133 0.1tx
1105 1134 Indicates server support for sending ``application/mercurial-0.1`` media
1106 1135 types.
1107 1136
1108 1137 0.2rx
1109 1138 Indicates server support for receiving ``application/mercurial-0.2`` media
1110 1139 types.
1111 1140
1112 1141 0.2tx
1113 1142 Indicates server support for sending ``application/mercurial-0.2`` media
1114 1143 types.
1115 1144
1116 1145 minrx=X
1117 1146 Minimum media type version the server is capable of receiving. Value is a
1118 1147 string like ``0.2``.
1119 1148
1120 1149 This capability can be used by servers to limit connections from legacy
1121 1150 clients not using the latest supported media type. However, only clients
1122 1151 with knowledge of this capability will know to consult this value. This
1123 1152 capability is present so the client may issue a more user-friendly error
1124 1153 when the server has locked out a legacy client.
1125 1154
1126 1155 mintx=X
1127 1156 Minimum media type version the server is capable of sending. Value is a
1128 1157 string like ``0.1``.
1129 1158
1130 1159 Servers advertising support for the ``application/mercurial-0.2`` media type
1131 1160 should also advertise the ``compression`` capability.
1132 1161
1133 1162 This capability was introduced in Mercurial 4.1 (released February 2017).
1134 1163
1135 1164 httppostargs
1136 1165 ------------
1137 1166
1138 1167 **Experimental**
1139 1168
1140 1169 Indicates that the server supports and prefers clients send command arguments
1141 1170 via a HTTP POST request as part of the request body.
1142 1171
1143 1172 This capability was introduced in Mercurial 3.8 (released May 2016).
1144 1173
1145 1174 known
1146 1175 -----
1147 1176
1148 1177 Whether the server supports the ``known`` command.
1149 1178
1150 1179 This capability/command was introduced in Mercurial 1.9 (released July 2011).
1151 1180
1152 1181 lookup
1153 1182 ------
1154 1183
1155 1184 Whether the server supports the ``lookup`` command.
1156 1185
1157 1186 This capability was introduced in Mercurial 0.9.2 (released December
1158 1187 2006).
1159 1188
1160 1189 This capability was introduced at the same time as the ``changegroupsubset``
1161 1190 capability/command.
1162 1191
1163 1192 partial-pull
1164 1193 ------------
1165 1194
1166 1195 Indicates that the client can deal with partial answers to pull requests
1167 1196 by repeating the request.
1168 1197
1169 1198 If this parameter is not advertised, the server will not send pull bundles.
1170 1199
1171 1200 This client capability was introduced in Mercurial 4.6.
1172 1201
1173 1202 protocaps
1174 1203 ---------
1175 1204
1176 1205 Whether the server supports the ``protocaps`` command for SSH V1 transport.
1177 1206
1178 1207 This capability was introduced in Mercurial 4.6.
1179 1208
1180 1209 pushkey
1181 1210 -------
1182 1211
1183 1212 Whether the server supports the ``pushkey`` and ``listkeys`` commands.
1184 1213
1185 1214 This capability was introduced in Mercurial 1.6 (released July 2010).
1186 1215
1187 1216 standardbundle
1188 1217 --------------
1189 1218
1190 1219 **Unsupported**
1191 1220
1192 1221 This capability was introduced during the Mercurial 0.9.2 development cycle in
1193 1222 2006. It was never present in a release, as it was replaced by the ``unbundle``
1194 1223 capability. This capability should not be encountered in the wild.
1195 1224
1196 1225 stream-preferred
1197 1226 ----------------
1198 1227
1199 1228 If present the server prefers that clients clone using the streaming clone
1200 1229 protocol (``hg clone --stream``) rather than the standard
1201 1230 changegroup/bundle based protocol.
1202 1231
1203 1232 This capability was introduced in Mercurial 2.2 (released May 2012).
1204 1233
1205 1234 streamreqs
1206 1235 ----------
1207 1236
1208 1237 Indicates whether the server supports *streaming clones* and the *requirements*
1209 1238 that clients must support to receive it.
1210 1239
1211 1240 If present, the server supports the ``stream_out`` command, which transmits
1212 1241 raw revlogs from the repository instead of changegroups. This provides a faster
1213 1242 cloning mechanism at the expense of more bandwidth used.
1214 1243
1215 1244 The value of this capability is a comma-delimited list of repo format
1216 1245 *requirements*. These are requirements that impact the reading of data in
1217 1246 the ``.hg/store`` directory. An example value is
1218 1247 ``streamreqs=generaldelta,revlogv1`` indicating the server repo requires
1219 1248 the ``revlogv1`` and ``generaldelta`` requirements.
1220 1249
1221 1250 If the only format requirement is ``revlogv1``, the server may expose the
1222 1251 ``stream`` capability instead of the ``streamreqs`` capability.
1223 1252
1224 1253 This capability was introduced in Mercurial 1.7 (released November 2010).
1225 1254
1226 1255 stream
1227 1256 ------
1228 1257
1229 1258 Whether the server supports *streaming clones* from ``revlogv1`` repos.
1230 1259
1231 1260 If present, the server supports the ``stream_out`` command, which transmits
1232 1261 raw revlogs from the repository instead of changegroups. This provides a faster
1233 1262 cloning mechanism at the expense of more bandwidth used.
1234 1263
1235 1264 This capability was introduced in Mercurial 0.9.1 (released July 2006).
1236 1265
1237 1266 When initially introduced, the value of the capability was the numeric
1238 1267 revlog revision. e.g. ``stream=1``. This indicates the changegroup is using
1239 1268 ``revlogv1``. This simple integer value wasn't powerful enough, so the
1240 1269 ``streamreqs`` capability was invented to handle cases where the repo
1241 1270 requirements have more than just ``revlogv1``. Newer servers omit the
1242 1271 ``=1`` since it was the only value supported and the value of ``1`` can
1243 1272 be implied by clients.
1244 1273
1245 1274 unbundlehash
1246 1275 ------------
1247 1276
1248 1277 Whether the ``unbundle`` commands supports receiving a hash of all the
1249 1278 heads instead of a list.
1250 1279
1251 1280 For more, see the documentation for the ``unbundle`` command.
1252 1281
1253 1282 This capability was introduced in Mercurial 1.9 (released July 2011).
1254 1283
1255 1284 unbundle
1256 1285 --------
1257 1286
1258 1287 Whether the server supports pushing via the ``unbundle`` command.
1259 1288
1260 1289 This capability/command has been present since Mercurial 0.9.1 (released
1261 1290 July 2006).
1262 1291
1263 1292 Mercurial 0.9.2 (released December 2006) added values to the capability
1264 1293 indicating which bundle types the server supports receiving. This value is a
1265 1294 comma-delimited list. e.g. ``HG10GZ,HG10BZ,HG10UN``. The order of values
1266 1295 reflects the priority/preference of that type, where the first value is the
1267 1296 most preferred type.
1268 1297
1269 1298 Content Negotiation
1270 1299 ===================
1271 1300
1272 1301 The wire protocol has some mechanisms to help peers determine what content
1273 1302 types and encoding the other side will accept. Historically, these mechanisms
1274 1303 have been built into commands themselves because most commands only send a
1275 1304 well-defined response type and only certain commands needed to support
1276 1305 functionality like compression.
1277 1306
1278 1307 Currently, only the HTTP version 1 transport supports content negotiation
1279 1308 at the protocol layer.
1280 1309
1281 1310 HTTP requests advertise supported response formats via the ``X-HgProto-<N>``
1282 1311 request header, where ``<N>`` is an integer starting at 1 allowing the logical
1283 1312 value to span multiple headers. This value consists of a list of
1284 1313 space-delimited parameters. Each parameter denotes a feature or capability.
1285 1314
1286 1315 The following parameters are defined:
1287 1316
1288 1317 0.1
1289 1318 Indicates the client supports receiving ``application/mercurial-0.1``
1290 1319 responses.
1291 1320
1292 1321 0.2
1293 1322 Indicates the client supports receiving ``application/mercurial-0.2``
1294 1323 responses.
1295 1324
1296 1325 cbor
1297 1326 Indicates the client supports receiving ``application/mercurial-cbor``
1298 1327 responses.
1299 1328
1300 1329 (Only intended to be used with version 2 transports.)
1301 1330
1302 1331 comp
1303 1332 Indicates compression formats the client can decode. Value is a list of
1304 1333 comma delimited strings identifying compression formats ordered from
1305 1334 most preferential to least preferential. e.g. ``comp=zstd,zlib,none``.
1306 1335
1307 1336 This parameter does not have an effect if only the ``0.1`` parameter
1308 1337 is defined, as support for ``application/mercurial-0.2`` or greater is
1309 1338 required to use arbitrary compression formats.
1310 1339
1311 1340 If this parameter is not advertised, the server interprets this as
1312 1341 equivalent to ``zlib,none``.
1313 1342
1314 1343 Clients may choose to only send this header if the ``httpmediatype``
1315 1344 server capability is present, as currently all server-side features
1316 1345 consulting this header require the client to opt in to new protocol features
1317 1346 advertised via the ``httpmediatype`` capability.
1318 1347
1319 1348 A server that doesn't receive an ``X-HgProto-<N>`` header should infer a
1320 1349 value of ``0.1``. This is compatible with legacy clients.
1321 1350
1322 1351 A server receiving a request indicating support for multiple media type
1323 1352 versions may respond with any of the supported media types. Not all servers
1324 1353 may support all media types on all commands.
1325 1354
1326 1355 Commands
1327 1356 ========
1328 1357
1329 1358 This section contains a list of all wire protocol commands implemented by
1330 1359 the canonical Mercurial server.
1331 1360
1332 1361 batch
1333 1362 -----
1334 1363
1335 1364 Issue multiple commands while sending a single command request. The purpose
1336 1365 of this command is to allow a client to issue multiple commands while avoiding
1337 1366 multiple round trips to the server therefore enabling commands to complete
1338 1367 quicker.
1339 1368
1340 1369 The command accepts a ``cmds`` argument that contains a list of commands to
1341 1370 execute.
1342 1371
1343 1372 The value of ``cmds`` is a ``;`` delimited list of strings. Each string has the
1344 1373 form ``<command> <arguments>``. That is, the command name followed by a space
1345 1374 followed by an argument string.
1346 1375
1347 1376 The argument string is a ``,`` delimited list of ``<key>=<value>`` values
1348 1377 corresponding to command arguments. Both the argument name and value are
1349 1378 escaped using a special substitution map::
1350 1379
1351 1380 : -> :c
1352 1381 , -> :o
1353 1382 ; -> :s
1354 1383 = -> :e
1355 1384
1356 1385 The response type for this command is ``string``. The value contains a
1357 1386 ``;`` delimited list of responses for each requested command. Each value
1358 1387 in this list is escaped using the same substitution map used for arguments.
1359 1388
1360 1389 If an error occurs, the generic error response may be sent.
1361 1390
1362 1391 between
1363 1392 -------
1364 1393
1365 1394 (Legacy command used for discovery in old clients)
1366 1395
1367 1396 Obtain nodes between pairs of nodes.
1368 1397
1369 1398 The ``pairs`` arguments contains a space-delimited list of ``-`` delimited
1370 1399 hex node pairs. e.g.::
1371 1400
1372 1401 a072279d3f7fd3a4aa7ffa1a5af8efc573e1c896-6dc58916e7c070f678682bfe404d2e2d68291a18
1373 1402
1374 1403 Return type is a ``string``. Value consists of lines corresponding to each
1375 1404 requested range. Each line contains a space-delimited list of hex nodes.
1376 1405 A newline ``\n`` terminates each line, including the last one.
1377 1406
1378 1407 branchmap
1379 1408 ---------
1380 1409
1381 1410 Obtain heads in named branches.
1382 1411
1383 1412 Accepts no arguments. Return type is a ``string``.
1384 1413
1385 1414 Return value contains lines with URL encoded branch names followed by a space
1386 1415 followed by a space-delimited list of hex nodes of heads on that branch.
1387 1416 e.g.::
1388 1417
1389 1418 default a072279d3f7fd3a4aa7ffa1a5af8efc573e1c896 6dc58916e7c070f678682bfe404d2e2d68291a18
1390 1419 stable baae3bf31522f41dd5e6d7377d0edd8d1cf3fccc
1391 1420
1392 1421 There is no trailing newline.
1393 1422
1394 1423 branches
1395 1424 --------
1396 1425
1397 1426 (Legacy command used for discovery in old clients. Clients with ``getbundle``
1398 1427 use the ``known`` and ``heads`` commands instead.)
1399 1428
1400 1429 Obtain ancestor changesets of specific nodes back to a branch point.
1401 1430
1402 1431 Despite the name, this command has nothing to do with Mercurial named branches.
1403 1432 Instead, it is related to DAG branches.
1404 1433
1405 1434 The command accepts a ``nodes`` argument, which is a string of space-delimited
1406 1435 hex nodes.
1407 1436
1408 1437 For each node requested, the server will find the first ancestor node that is
1409 1438 a DAG root or is a merge.
1410 1439
1411 1440 Return type is a ``string``. Return value contains lines with result data for
1412 1441 each requested node. Each line contains space-delimited nodes followed by a
1413 1442 newline (``\n``). The 4 nodes reported on each line correspond to the requested
1414 1443 node, the ancestor node found, and its 2 parent nodes (which may be the null
1415 1444 node).
1416 1445
1417 1446 capabilities
1418 1447 ------------
1419 1448
1420 1449 Obtain the capabilities string for the repo.
1421 1450
1422 1451 Unlike the ``hello`` command, the capabilities string is not prefixed.
1423 1452 There is no trailing newline.
1424 1453
1425 1454 This command does not accept any arguments. Return type is a ``string``.
1426 1455
1427 1456 This command was introduced in Mercurial 0.9.1 (released July 2006).
1428 1457
1429 1458 changegroup
1430 1459 -----------
1431 1460
1432 1461 (Legacy command: use ``getbundle`` instead)
1433 1462
1434 1463 Obtain a changegroup version 1 with data for changesets that are
1435 1464 descendants of client-specified changesets.
1436 1465
1437 1466 The ``roots`` arguments contains a list of space-delimited hex nodes.
1438 1467
1439 1468 The server responds with a changegroup version 1 containing all
1440 1469 changesets between the requested root/base nodes and the repo's head nodes
1441 1470 at the time of the request.
1442 1471
1443 1472 The return type is a ``stream``.
1444 1473
1445 1474 changegroupsubset
1446 1475 -----------------
1447 1476
1448 1477 (Legacy command: use ``getbundle`` instead)
1449 1478
1450 1479 Obtain a changegroup version 1 with data for changesetsets between
1451 1480 client specified base and head nodes.
1452 1481
1453 1482 The ``bases`` argument contains a list of space-delimited hex nodes.
1454 1483 The ``heads`` argument contains a list of space-delimited hex nodes.
1455 1484
1456 1485 The server responds with a changegroup version 1 containing all
1457 1486 changesets between the requested base and head nodes at the time of the
1458 1487 request.
1459 1488
1460 1489 The return type is a ``stream``.
1461 1490
1462 1491 clonebundles
1463 1492 ------------
1464 1493
1465 1494 Obtains a manifest of bundle URLs available to seed clones.
1466 1495
1467 1496 Each returned line contains a URL followed by metadata. See the
1468 1497 documentation in the ``clonebundles`` extension for more.
1469 1498
1470 1499 The return type is a ``string``.
1471 1500
1472 1501 getbundle
1473 1502 ---------
1474 1503
1475 1504 Obtain a bundle containing repository data.
1476 1505
1477 1506 This command accepts the following arguments:
1478 1507
1479 1508 heads
1480 1509 List of space-delimited hex nodes of heads to retrieve.
1481 1510 common
1482 1511 List of space-delimited hex nodes that the client has in common with the
1483 1512 server.
1484 1513 obsmarkers
1485 1514 Boolean indicating whether to include obsolescence markers as part
1486 1515 of the response. Only works with bundle2.
1487 1516 bundlecaps
1488 1517 Comma-delimited set of strings defining client bundle capabilities.
1489 1518 listkeys
1490 1519 Comma-delimited list of strings of ``pushkey`` namespaces. For each
1491 1520 namespace listed, a bundle2 part will be included with the content of
1492 1521 that namespace.
1493 1522 cg
1494 1523 Boolean indicating whether changegroup data is requested.
1495 1524 cbattempted
1496 1525 Boolean indicating whether the client attempted to use the *clone bundles*
1497 1526 feature before performing this request.
1498 1527 bookmarks
1499 1528 Boolean indicating whether bookmark data is requested.
1500 1529 phases
1501 1530 Boolean indicating whether phases data is requested.
1502 1531
1503 1532 The return type on success is a ``stream`` where the value is bundle.
1504 1533 On the HTTP version 1 transport, the response is zlib compressed.
1505 1534
1506 1535 If an error occurs, a generic error response can be sent.
1507 1536
1508 1537 Unless the client sends a false value for the ``cg`` argument, the returned
1509 1538 bundle contains a changegroup with the nodes between the specified ``common``
1510 1539 and ``heads`` nodes. Depending on the command arguments, the type and content
1511 1540 of the returned bundle can vary significantly.
1512 1541
1513 1542 The default behavior is for the server to send a raw changegroup version
1514 1543 ``01`` response.
1515 1544
1516 1545 If the ``bundlecaps`` provided by the client contain a value beginning
1517 1546 with ``HG2``, a bundle2 will be returned. The bundle2 data may contain
1518 1547 additional repository data, such as ``pushkey`` namespace values.
1519 1548
1520 1549 heads
1521 1550 -----
1522 1551
1523 1552 Returns a list of space-delimited hex nodes of repository heads followed
1524 1553 by a newline. e.g.
1525 1554 ``a9eeb3adc7ddb5006c088e9eda61791c777cbf7c 31f91a3da534dc849f0d6bfc00a395a97cf218a1\n``
1526 1555
1527 1556 This command does not accept any arguments. The return type is a ``string``.
1528 1557
1529 1558 hello
1530 1559 -----
1531 1560
1532 1561 Returns lines describing interesting things about the server in an RFC-822
1533 1562 like format.
1534 1563
1535 1564 Currently, the only line defines the server capabilities. It has the form::
1536 1565
1537 1566 capabilities: <value>
1538 1567
1539 1568 See above for more about the capabilities string.
1540 1569
1541 1570 SSH clients typically issue this command as soon as a connection is
1542 1571 established.
1543 1572
1544 1573 This command does not accept any arguments. The return type is a ``string``.
1545 1574
1546 1575 This command was introduced in Mercurial 0.9.1 (released July 2006).
1547 1576
1548 1577 listkeys
1549 1578 --------
1550 1579
1551 1580 List values in a specified ``pushkey`` namespace.
1552 1581
1553 1582 The ``namespace`` argument defines the pushkey namespace to operate on.
1554 1583
1555 1584 The return type is a ``string``. The value is an encoded dictionary of keys.
1556 1585
1557 1586 Key-value pairs are delimited by newlines (``\n``). Within each line, keys and
1558 1587 values are separated by a tab (``\t``). Keys and values are both strings.
1559 1588
1560 1589 lookup
1561 1590 ------
1562 1591
1563 1592 Try to resolve a value to a known repository revision.
1564 1593
1565 1594 The ``key`` argument is converted from bytes to an
1566 1595 ``encoding.localstr`` instance then passed into
1567 1596 ``localrepository.__getitem__`` in an attempt to resolve it.
1568 1597
1569 1598 The return type is a ``string``.
1570 1599
1571 1600 Upon successful resolution, returns ``1 <hex node>\n``. On failure,
1572 1601 returns ``0 <error string>\n``. e.g.::
1573 1602
1574 1603 1 273ce12ad8f155317b2c078ec75a4eba507f1fba\n
1575 1604
1576 1605 0 unknown revision 'foo'\n
1577 1606
1578 1607 known
1579 1608 -----
1580 1609
1581 1610 Determine whether multiple nodes are known.
1582 1611
1583 1612 The ``nodes`` argument is a list of space-delimited hex nodes to check
1584 1613 for existence.
1585 1614
1586 1615 The return type is ``string``.
1587 1616
1588 1617 Returns a string consisting of ``0``s and ``1``s indicating whether nodes
1589 1618 are known. If the Nth node specified in the ``nodes`` argument is known,
1590 1619 a ``1`` will be returned at byte offset N. If the node isn't known, ``0``
1591 1620 will be present at byte offset N.
1592 1621
1593 1622 There is no trailing newline.
1594 1623
1595 1624 protocaps
1596 1625 ---------
1597 1626
1598 1627 Notify the server about the client capabilities in the SSH V1 transport
1599 1628 protocol.
1600 1629
1601 1630 The ``caps`` argument is a space-delimited list of capabilities.
1602 1631
1603 1632 The server will reply with the string ``OK``.
1604 1633
1605 1634 pushkey
1606 1635 -------
1607 1636
1608 1637 Set a value using the ``pushkey`` protocol.
1609 1638
1610 1639 Accepts arguments ``namespace``, ``key``, ``old``, and ``new``, which
1611 1640 correspond to the pushkey namespace to operate on, the key within that
1612 1641 namespace to change, the old value (which may be empty), and the new value.
1613 1642 All arguments are string types.
1614 1643
1615 1644 The return type is a ``string``. The value depends on the transport protocol.
1616 1645
1617 1646 The SSH version 1 transport sends a string encoded integer followed by a
1618 1647 newline (``\n``) which indicates operation result. The server may send
1619 1648 additional output on the ``stderr`` stream that should be displayed to the
1620 1649 user.
1621 1650
1622 1651 The HTTP version 1 transport sends a string encoded integer followed by a
1623 1652 newline followed by additional server output that should be displayed to
1624 1653 the user. This may include output from hooks, etc.
1625 1654
1626 1655 The integer result varies by namespace. ``0`` means an error has occurred
1627 1656 and there should be additional output to display to the user.
1628 1657
1629 1658 stream_out
1630 1659 ----------
1631 1660
1632 1661 Obtain *streaming clone* data.
1633 1662
1634 1663 The return type is either a ``string`` or a ``stream``, depending on
1635 1664 whether the request was fulfilled properly.
1636 1665
1637 1666 A return value of ``1\n`` indicates the server is not configured to serve
1638 1667 this data. If this is seen by the client, they may not have verified the
1639 1668 ``stream`` capability is set before making the request.
1640 1669
1641 1670 A return value of ``2\n`` indicates the server was unable to lock the
1642 1671 repository to generate data.
1643 1672
1644 1673 All other responses are a ``stream`` of bytes. The first line of this data
1645 1674 contains 2 space-delimited integers corresponding to the path count and
1646 1675 payload size, respectively::
1647 1676
1648 1677 <path count> <payload size>\n
1649 1678
1650 1679 The ``<payload size>`` is the total size of path data: it does not include
1651 1680 the size of the per-path header lines.
1652 1681
1653 1682 Following that header are ``<path count>`` entries. Each entry consists of a
1654 1683 line with metadata followed by raw revlog data. The line consists of::
1655 1684
1656 1685 <store path>\0<size>\n
1657 1686
1658 1687 The ``<store path>`` is the encoded store path of the data that follows.
1659 1688 ``<size>`` is the amount of data for this store path/revlog that follows the
1660 1689 newline.
1661 1690
1662 1691 There is no trailer to indicate end of data. Instead, the client should stop
1663 1692 reading after ``<path count>`` entries are consumed.
1664 1693
1665 1694 unbundle
1666 1695 --------
1667 1696
1668 1697 Send a bundle containing data (usually changegroup data) to the server.
1669 1698
1670 1699 Accepts the argument ``heads``, which is a space-delimited list of hex nodes
1671 1700 corresponding to server repository heads observed by the client. This is used
1672 1701 to detect race conditions and abort push operations before a server performs
1673 1702 too much work or a client transfers too much data.
1674 1703
1675 1704 The request payload consists of a bundle to be applied to the repository,
1676 1705 similarly to as if :hg:`unbundle` were called.
1677 1706
1678 1707 In most scenarios, a special ``push response`` type is returned. This type
1679 1708 contains an integer describing the change in heads as a result of the
1680 1709 operation. A value of ``0`` indicates nothing changed. ``1`` means the number
1681 1710 of heads remained the same. Values ``2`` and larger indicate the number of
1682 1711 added heads minus 1. e.g. ``3`` means 2 heads were added. Negative values
1683 1712 indicate the number of fewer heads, also off by 1. e.g. ``-2`` means there
1684 1713 is 1 fewer head.
1685 1714
1686 1715 The encoding of the ``push response`` type varies by transport.
1687 1716
1688 1717 For the SSH version 1 transport, this type is composed of 2 ``string``
1689 1718 responses: an empty response (``0\n``) followed by the integer result value.
1690 1719 e.g. ``1\n2``. So the full response might be ``0\n1\n2``.
1691 1720
1692 1721 For the HTTP version 1 transport, the response is a ``string`` type composed
1693 1722 of an integer result value followed by a newline (``\n``) followed by string
1694 1723 content holding server output that should be displayed on the client (output
1695 1724 hooks, etc).
1696 1725
1697 1726 In some cases, the server may respond with a ``bundle2`` bundle. In this
1698 1727 case, the response type is ``stream``. For the HTTP version 1 transport, the
1699 1728 response is zlib compressed.
1700 1729
1701 1730 The server may also respond with a generic error type, which contains a string
1702 1731 indicating the failure.
1703 1732
1704 1733 Frame-Based Protocol Commands
1705 1734 =============================
1706 1735
1707 1736 **Experimental and under active development**
1708 1737
1709 1738 This section documents the wire protocol commands exposed to transports
1710 1739 using the frame-based protocol. The set of commands exposed through
1711 1740 these transports is distinct from the set of commands exposed to legacy
1712 1741 transports.
1713 1742
1714 1743 The frame-based protocol uses CBOR to encode command execution requests.
1715 1744 All command arguments must be mapped to a specific or set of CBOR data
1716 1745 types.
1717 1746
1718 1747 The response to many commands is also CBOR. There is no common response
1719 1748 format: each command defines its own response format.
1720 1749
1721 1750 TODO require node type be specified, as N bytes of binary node value
1722 1751 could be ambiguous once SHA-1 is replaced.
1723 1752
1724 1753 branchmap
1725 1754 ---------
1726 1755
1727 1756 Obtain heads in named branches.
1728 1757
1729 1758 Receives no arguments.
1730 1759
1731 1760 The response is a map with bytestring keys defining the branch name.
1732 1761 Values are arrays of bytestring defining raw changeset nodes.
1733 1762
1734 1763 capabilities
1735 1764 ------------
1736 1765
1737 1766 Obtain the server's capabilities.
1738 1767
1739 1768 Receives no arguments.
1740 1769
1741 1770 This command is typically called only as part of the handshake during
1742 1771 initial connection establishment.
1743 1772
1744 1773 The response is a map with bytestring keys defining server information.
1745 1774
1746 1775 The defined keys are:
1747 1776
1748 1777 commands
1749 1778 A map defining available wire protocol commands on this server.
1750 1779
1751 1780 Keys in the map are the names of commands that can be invoked. Values
1752 1781 are maps defining information about that command. The bytestring keys
1753 1782 are:
1754 1783
1755 1784 args
1756 1785 A map of argument names and their expected types.
1757 1786
1758 1787 Types are defined as a representative value for the expected type.
1759 1788 e.g. an argument expecting a boolean type will have its value
1760 1789 set to true. An integer type will have its value set to 42. The
1761 1790 actual values are arbitrary and may not have meaning.
1762 1791 permissions
1763 1792 An array of permissions required to execute this command.
1764 1793
1765 1794 compression
1766 1795 An array of maps defining available compression format support.
1767 1796
1768 1797 The array is sorted from most preferred to least preferred.
1769 1798
1770 1799 Each entry has the following bytestring keys:
1771 1800
1772 1801 name
1773 1802 Name of the compression engine. e.g. ``zstd`` or ``zlib``.
1774 1803
1775 1804 framingmediatypes
1776 1805 An array of bytestrings defining the supported framing protocol
1777 1806 media types. Servers will not accept media types not in this list.
1778 1807
1779 1808 rawrepoformats
1780 1809 An array of storage formats the repository is using. This set of
1781 1810 requirements can be used to determine whether a client can read a
1782 1811 *raw* copy of file data available.
1783 1812
1784 1813 heads
1785 1814 -----
1786 1815
1787 1816 Obtain DAG heads in the repository.
1788 1817
1789 1818 The command accepts the following arguments:
1790 1819
1791 1820 publiconly (optional)
1792 1821 (boolean) If set, operate on the DAG for public phase changesets only.
1793 1822 Non-public (i.e. draft) phase DAG heads will not be returned.
1794 1823
1795 1824 The response is a CBOR array of bytestrings defining changeset nodes
1796 1825 of DAG heads. The array can be empty if the repository is empty or no
1797 1826 changesets satisfied the request.
1798 1827
1799 1828 TODO consider exposing phase of heads in response
1800 1829
1801 1830 known
1802 1831 -----
1803 1832
1804 1833 Determine whether a series of changeset nodes is known to the server.
1805 1834
1806 1835 The command accepts the following arguments:
1807 1836
1808 1837 nodes
1809 1838 (array of bytestrings) List of changeset nodes whose presence to
1810 1839 query.
1811 1840
1812 1841 The response is a bytestring where each byte contains a 0 or 1 for the
1813 1842 corresponding requested node at the same index.
1814 1843
1815 1844 TODO use a bit array for even more compact response
1816 1845
1817 1846 listkeys
1818 1847 --------
1819 1848
1820 1849 List values in a specified ``pushkey`` namespace.
1821 1850
1822 1851 The command receives the following arguments:
1823 1852
1824 1853 namespace
1825 1854 (bytestring) Pushkey namespace to query.
1826 1855
1827 1856 The response is a map with bytestring keys and values.
1828 1857
1829 1858 TODO consider using binary to represent nodes in certain pushkey namespaces.
1830 1859
1831 1860 lookup
1832 1861 ------
1833 1862
1834 1863 Try to resolve a value to a changeset revision.
1835 1864
1836 1865 Unlike ``known`` which operates on changeset nodes, lookup operates on
1837 1866 node fragments and other names that a user may use.
1838 1867
1839 1868 The command receives the following arguments:
1840 1869
1841 1870 key
1842 1871 (bytestring) Value to try to resolve.
1843 1872
1844 1873 On success, returns a bytestring containing the resolved node.
1845 1874
1846 1875 pushkey
1847 1876 -------
1848 1877
1849 1878 Set a value using the ``pushkey`` protocol.
1850 1879
1851 1880 The command receives the following arguments:
1852 1881
1853 1882 namespace
1854 1883 (bytestring) Pushkey namespace to operate on.
1855 1884 key
1856 1885 (bytestring) The pushkey key to set.
1857 1886 old
1858 1887 (bytestring) Old value for this key.
1859 1888 new
1860 1889 (bytestring) New value for this key.
1861 1890
1862 1891 TODO consider using binary to represent nodes is certain pushkey namespaces.
1863 1892 TODO better define response type and meaning.
@@ -1,1062 +1,1073 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 cbor,
21 21 )
22 22 from . import (
23 23 encoding,
24 24 error,
25 25 util,
26 26 )
27 27 from .utils import (
28 28 stringutil,
29 29 )
30 30
31 31 FRAME_HEADER_SIZE = 8
32 32 DEFAULT_MAX_FRAME_SIZE = 32768
33 33
34 34 STREAM_FLAG_BEGIN_STREAM = 0x01
35 35 STREAM_FLAG_END_STREAM = 0x02
36 36 STREAM_FLAG_ENCODING_APPLIED = 0x04
37 37
38 38 STREAM_FLAGS = {
39 39 b'stream-begin': STREAM_FLAG_BEGIN_STREAM,
40 40 b'stream-end': STREAM_FLAG_END_STREAM,
41 41 b'encoded': STREAM_FLAG_ENCODING_APPLIED,
42 42 }
43 43
44 44 FRAME_TYPE_COMMAND_REQUEST = 0x01
45 45 FRAME_TYPE_COMMAND_DATA = 0x02
46 46 FRAME_TYPE_COMMAND_RESPONSE = 0x03
47 47 FRAME_TYPE_ERROR_RESPONSE = 0x05
48 48 FRAME_TYPE_TEXT_OUTPUT = 0x06
49 49 FRAME_TYPE_PROGRESS = 0x07
50 50 FRAME_TYPE_STREAM_SETTINGS = 0x08
51 51
52 52 FRAME_TYPES = {
53 53 b'command-request': FRAME_TYPE_COMMAND_REQUEST,
54 54 b'command-data': FRAME_TYPE_COMMAND_DATA,
55 55 b'command-response': FRAME_TYPE_COMMAND_RESPONSE,
56 56 b'error-response': FRAME_TYPE_ERROR_RESPONSE,
57 57 b'text-output': FRAME_TYPE_TEXT_OUTPUT,
58 58 b'progress': FRAME_TYPE_PROGRESS,
59 59 b'stream-settings': FRAME_TYPE_STREAM_SETTINGS,
60 60 }
61 61
62 62 FLAG_COMMAND_REQUEST_NEW = 0x01
63 63 FLAG_COMMAND_REQUEST_CONTINUATION = 0x02
64 64 FLAG_COMMAND_REQUEST_MORE_FRAMES = 0x04
65 65 FLAG_COMMAND_REQUEST_EXPECT_DATA = 0x08
66 66
67 67 FLAGS_COMMAND_REQUEST = {
68 68 b'new': FLAG_COMMAND_REQUEST_NEW,
69 69 b'continuation': FLAG_COMMAND_REQUEST_CONTINUATION,
70 70 b'more': FLAG_COMMAND_REQUEST_MORE_FRAMES,
71 71 b'have-data': FLAG_COMMAND_REQUEST_EXPECT_DATA,
72 72 }
73 73
74 74 FLAG_COMMAND_DATA_CONTINUATION = 0x01
75 75 FLAG_COMMAND_DATA_EOS = 0x02
76 76
77 77 FLAGS_COMMAND_DATA = {
78 78 b'continuation': FLAG_COMMAND_DATA_CONTINUATION,
79 79 b'eos': FLAG_COMMAND_DATA_EOS,
80 80 }
81 81
82 82 FLAG_COMMAND_RESPONSE_CONTINUATION = 0x01
83 83 FLAG_COMMAND_RESPONSE_EOS = 0x02
84 84
85 85 FLAGS_COMMAND_RESPONSE = {
86 86 b'continuation': FLAG_COMMAND_RESPONSE_CONTINUATION,
87 87 b'eos': FLAG_COMMAND_RESPONSE_EOS,
88 88 }
89 89
90 90 FLAG_ERROR_RESPONSE_PROTOCOL = 0x01
91 91 FLAG_ERROR_RESPONSE_APPLICATION = 0x02
92 92
93 93 FLAGS_ERROR_RESPONSE = {
94 94 b'protocol': FLAG_ERROR_RESPONSE_PROTOCOL,
95 95 b'application': FLAG_ERROR_RESPONSE_APPLICATION,
96 96 }
97 97
98 98 # Maps frame types to their available flags.
99 99 FRAME_TYPE_FLAGS = {
100 100 FRAME_TYPE_COMMAND_REQUEST: FLAGS_COMMAND_REQUEST,
101 101 FRAME_TYPE_COMMAND_DATA: FLAGS_COMMAND_DATA,
102 102 FRAME_TYPE_COMMAND_RESPONSE: FLAGS_COMMAND_RESPONSE,
103 103 FRAME_TYPE_ERROR_RESPONSE: FLAGS_ERROR_RESPONSE,
104 104 FRAME_TYPE_TEXT_OUTPUT: {},
105 105 FRAME_TYPE_PROGRESS: {},
106 106 FRAME_TYPE_STREAM_SETTINGS: {},
107 107 }
108 108
109 109 ARGUMENT_RECORD_HEADER = struct.Struct(r'<HH')
110 110
111 111 def humanflags(mapping, value):
112 112 """Convert a numeric flags value to a human value, using a mapping table."""
113 113 namemap = {v: k for k, v in mapping.iteritems()}
114 114 flags = []
115 115 val = 1
116 116 while value >= val:
117 117 if value & val:
118 118 flags.append(namemap.get(val, '<unknown 0x%02x>' % val))
119 119 val <<= 1
120 120
121 121 return b'|'.join(flags)
122 122
123 123 @attr.s(slots=True)
124 124 class frameheader(object):
125 125 """Represents the data in a frame header."""
126 126
127 127 length = attr.ib()
128 128 requestid = attr.ib()
129 129 streamid = attr.ib()
130 130 streamflags = attr.ib()
131 131 typeid = attr.ib()
132 132 flags = attr.ib()
133 133
134 134 @attr.s(slots=True, repr=False)
135 135 class frame(object):
136 136 """Represents a parsed frame."""
137 137
138 138 requestid = attr.ib()
139 139 streamid = attr.ib()
140 140 streamflags = attr.ib()
141 141 typeid = attr.ib()
142 142 flags = attr.ib()
143 143 payload = attr.ib()
144 144
145 145 @encoding.strmethod
146 146 def __repr__(self):
147 147 typename = '<unknown 0x%02x>' % self.typeid
148 148 for name, value in FRAME_TYPES.iteritems():
149 149 if value == self.typeid:
150 150 typename = name
151 151 break
152 152
153 153 return ('frame(size=%d; request=%d; stream=%d; streamflags=%s; '
154 154 'type=%s; flags=%s)' % (
155 155 len(self.payload), self.requestid, self.streamid,
156 156 humanflags(STREAM_FLAGS, self.streamflags), typename,
157 157 humanflags(FRAME_TYPE_FLAGS.get(self.typeid, {}), self.flags)))
158 158
159 159 def makeframe(requestid, streamid, streamflags, typeid, flags, payload):
160 160 """Assemble a frame into a byte array."""
161 161 # TODO assert size of payload.
162 162 frame = bytearray(FRAME_HEADER_SIZE + len(payload))
163 163
164 164 # 24 bits length
165 165 # 16 bits request id
166 166 # 8 bits stream id
167 167 # 8 bits stream flags
168 168 # 4 bits type
169 169 # 4 bits flags
170 170
171 171 l = struct.pack(r'<I', len(payload))
172 172 frame[0:3] = l[0:3]
173 173 struct.pack_into(r'<HBB', frame, 3, requestid, streamid, streamflags)
174 174 frame[7] = (typeid << 4) | flags
175 175 frame[8:] = payload
176 176
177 177 return frame
178 178
179 179 def makeframefromhumanstring(s):
180 180 """Create a frame from a human readable string
181 181
182 182 Strings have the form:
183 183
184 184 <request-id> <stream-id> <stream-flags> <type> <flags> <payload>
185 185
186 186 This can be used by user-facing applications and tests for creating
187 187 frames easily without having to type out a bunch of constants.
188 188
189 189 Request ID and stream IDs are integers.
190 190
191 191 Stream flags, frame type, and flags can be specified by integer or
192 192 named constant.
193 193
194 194 Flags can be delimited by `|` to bitwise OR them together.
195 195
196 196 If the payload begins with ``cbor:``, the following string will be
197 197 evaluated as Python literal and the resulting object will be fed into
198 198 a CBOR encoder. Otherwise, the payload is interpreted as a Python
199 199 byte string literal.
200 200 """
201 201 fields = s.split(b' ', 5)
202 202 requestid, streamid, streamflags, frametype, frameflags, payload = fields
203 203
204 204 requestid = int(requestid)
205 205 streamid = int(streamid)
206 206
207 207 finalstreamflags = 0
208 208 for flag in streamflags.split(b'|'):
209 209 if flag in STREAM_FLAGS:
210 210 finalstreamflags |= STREAM_FLAGS[flag]
211 211 else:
212 212 finalstreamflags |= int(flag)
213 213
214 214 if frametype in FRAME_TYPES:
215 215 frametype = FRAME_TYPES[frametype]
216 216 else:
217 217 frametype = int(frametype)
218 218
219 219 finalflags = 0
220 220 validflags = FRAME_TYPE_FLAGS[frametype]
221 221 for flag in frameflags.split(b'|'):
222 222 if flag in validflags:
223 223 finalflags |= validflags[flag]
224 224 else:
225 225 finalflags |= int(flag)
226 226
227 227 if payload.startswith(b'cbor:'):
228 228 payload = cbor.dumps(stringutil.evalpythonliteral(payload[5:]),
229 229 canonical=True)
230 230
231 231 else:
232 232 payload = stringutil.unescapestr(payload)
233 233
234 234 return makeframe(requestid=requestid, streamid=streamid,
235 235 streamflags=finalstreamflags, typeid=frametype,
236 236 flags=finalflags, payload=payload)
237 237
238 238 def parseheader(data):
239 239 """Parse a unified framing protocol frame header from a buffer.
240 240
241 241 The header is expected to be in the buffer at offset 0 and the
242 242 buffer is expected to be large enough to hold a full header.
243 243 """
244 244 # 24 bits payload length (little endian)
245 245 # 16 bits request ID
246 246 # 8 bits stream ID
247 247 # 8 bits stream flags
248 248 # 4 bits frame type
249 249 # 4 bits frame flags
250 250 # ... payload
251 251 framelength = data[0] + 256 * data[1] + 16384 * data[2]
252 252 requestid, streamid, streamflags = struct.unpack_from(r'<HBB', data, 3)
253 253 typeflags = data[7]
254 254
255 255 frametype = (typeflags & 0xf0) >> 4
256 256 frameflags = typeflags & 0x0f
257 257
258 258 return frameheader(framelength, requestid, streamid, streamflags,
259 259 frametype, frameflags)
260 260
261 261 def readframe(fh):
262 262 """Read a unified framing protocol frame from a file object.
263 263
264 264 Returns a 3-tuple of (type, flags, payload) for the decoded frame or
265 265 None if no frame is available. May raise if a malformed frame is
266 266 seen.
267 267 """
268 268 header = bytearray(FRAME_HEADER_SIZE)
269 269
270 270 readcount = fh.readinto(header)
271 271
272 272 if readcount == 0:
273 273 return None
274 274
275 275 if readcount != FRAME_HEADER_SIZE:
276 276 raise error.Abort(_('received incomplete frame: got %d bytes: %s') %
277 277 (readcount, header))
278 278
279 279 h = parseheader(header)
280 280
281 281 payload = fh.read(h.length)
282 282 if len(payload) != h.length:
283 283 raise error.Abort(_('frame length error: expected %d; got %d') %
284 284 (h.length, len(payload)))
285 285
286 286 return frame(h.requestid, h.streamid, h.streamflags, h.typeid, h.flags,
287 287 payload)
288 288
289 289 def createcommandframes(stream, requestid, cmd, args, datafh=None,
290 290 maxframesize=DEFAULT_MAX_FRAME_SIZE):
291 291 """Create frames necessary to transmit a request to run a command.
292 292
293 293 This is a generator of bytearrays. Each item represents a frame
294 294 ready to be sent over the wire to a peer.
295 295 """
296 296 data = {b'name': cmd}
297 297 if args:
298 298 data[b'args'] = args
299 299
300 300 data = cbor.dumps(data, canonical=True)
301 301
302 302 offset = 0
303 303
304 304 while True:
305 305 flags = 0
306 306
307 307 # Must set new or continuation flag.
308 308 if not offset:
309 309 flags |= FLAG_COMMAND_REQUEST_NEW
310 310 else:
311 311 flags |= FLAG_COMMAND_REQUEST_CONTINUATION
312 312
313 313 # Data frames is set on all frames.
314 314 if datafh:
315 315 flags |= FLAG_COMMAND_REQUEST_EXPECT_DATA
316 316
317 317 payload = data[offset:offset + maxframesize]
318 318 offset += len(payload)
319 319
320 320 if len(payload) == maxframesize and offset < len(data):
321 321 flags |= FLAG_COMMAND_REQUEST_MORE_FRAMES
322 322
323 323 yield stream.makeframe(requestid=requestid,
324 324 typeid=FRAME_TYPE_COMMAND_REQUEST,
325 325 flags=flags,
326 326 payload=payload)
327 327
328 328 if not (flags & FLAG_COMMAND_REQUEST_MORE_FRAMES):
329 329 break
330 330
331 331 if datafh:
332 332 while True:
333 333 data = datafh.read(DEFAULT_MAX_FRAME_SIZE)
334 334
335 335 done = False
336 336 if len(data) == DEFAULT_MAX_FRAME_SIZE:
337 337 flags = FLAG_COMMAND_DATA_CONTINUATION
338 338 else:
339 339 flags = FLAG_COMMAND_DATA_EOS
340 340 assert datafh.read(1) == b''
341 341 done = True
342 342
343 343 yield stream.makeframe(requestid=requestid,
344 344 typeid=FRAME_TYPE_COMMAND_DATA,
345 345 flags=flags,
346 346 payload=data)
347 347
348 348 if done:
349 349 break
350 350
351 351 def createcommandresponseframesfrombytes(stream, requestid, data,
352 352 maxframesize=DEFAULT_MAX_FRAME_SIZE):
353 353 """Create a raw frame to send a bytes response from static bytes input.
354 354
355 355 Returns a generator of bytearrays.
356 356 """
357 # Automatically send the overall CBOR response map.
358 overall = cbor.dumps({b'status': b'ok'}, canonical=True)
359 if len(overall) > maxframesize:
360 raise error.ProgrammingError('not yet implemented')
357 361
358 # Simple case of a single frame.
359 if len(data) <= maxframesize:
362 # Simple case where we can fit the full response in a single frame.
363 if len(overall) + len(data) <= maxframesize:
360 364 flags = FLAG_COMMAND_RESPONSE_EOS
361 365 yield stream.makeframe(requestid=requestid,
362 366 typeid=FRAME_TYPE_COMMAND_RESPONSE,
363 367 flags=flags,
364 payload=data)
368 payload=overall + data)
365 369 return
366 370
371 # It's easier to send the overall CBOR map in its own frame than to track
372 # offsets.
373 yield stream.makeframe(requestid=requestid,
374 typeid=FRAME_TYPE_COMMAND_RESPONSE,
375 flags=FLAG_COMMAND_RESPONSE_CONTINUATION,
376 payload=overall)
377
367 378 offset = 0
368 379 while True:
369 380 chunk = data[offset:offset + maxframesize]
370 381 offset += len(chunk)
371 382 done = offset == len(data)
372 383
373 384 if done:
374 385 flags = FLAG_COMMAND_RESPONSE_EOS
375 386 else:
376 387 flags = FLAG_COMMAND_RESPONSE_CONTINUATION
377 388
378 389 yield stream.makeframe(requestid=requestid,
379 390 typeid=FRAME_TYPE_COMMAND_RESPONSE,
380 391 flags=flags,
381 392 payload=chunk)
382 393
383 394 if done:
384 395 break
385 396
386 397 def createerrorframe(stream, requestid, msg, protocol=False, application=False):
387 398 # TODO properly handle frame size limits.
388 399 assert len(msg) <= DEFAULT_MAX_FRAME_SIZE
389 400
390 401 flags = 0
391 402 if protocol:
392 403 flags |= FLAG_ERROR_RESPONSE_PROTOCOL
393 404 if application:
394 405 flags |= FLAG_ERROR_RESPONSE_APPLICATION
395 406
396 407 yield stream.makeframe(requestid=requestid,
397 408 typeid=FRAME_TYPE_ERROR_RESPONSE,
398 409 flags=flags,
399 410 payload=msg)
400 411
401 412 def createtextoutputframe(stream, requestid, atoms,
402 413 maxframesize=DEFAULT_MAX_FRAME_SIZE):
403 414 """Create a text output frame to render text to people.
404 415
405 416 ``atoms`` is a 3-tuple of (formatting string, args, labels).
406 417
407 418 The formatting string contains ``%s`` tokens to be replaced by the
408 419 corresponding indexed entry in ``args``. ``labels`` is an iterable of
409 420 formatters to be applied at rendering time. In terms of the ``ui``
410 421 class, each atom corresponds to a ``ui.write()``.
411 422 """
412 423 atomdicts = []
413 424
414 425 for (formatting, args, labels) in atoms:
415 426 # TODO look for localstr, other types here?
416 427
417 428 if not isinstance(formatting, bytes):
418 429 raise ValueError('must use bytes formatting strings')
419 430 for arg in args:
420 431 if not isinstance(arg, bytes):
421 432 raise ValueError('must use bytes for arguments')
422 433 for label in labels:
423 434 if not isinstance(label, bytes):
424 435 raise ValueError('must use bytes for labels')
425 436
426 437 # Formatting string must be ASCII.
427 438 formatting = formatting.decode(r'ascii', r'replace').encode(r'ascii')
428 439
429 440 # Arguments must be UTF-8.
430 441 args = [a.decode(r'utf-8', r'replace').encode(r'utf-8') for a in args]
431 442
432 443 # Labels must be ASCII.
433 444 labels = [l.decode(r'ascii', r'strict').encode(r'ascii')
434 445 for l in labels]
435 446
436 447 atom = {b'msg': formatting}
437 448 if args:
438 449 atom[b'args'] = args
439 450 if labels:
440 451 atom[b'labels'] = labels
441 452
442 453 atomdicts.append(atom)
443 454
444 455 payload = cbor.dumps(atomdicts, canonical=True)
445 456
446 457 if len(payload) > maxframesize:
447 458 raise ValueError('cannot encode data in a single frame')
448 459
449 460 yield stream.makeframe(requestid=requestid,
450 461 typeid=FRAME_TYPE_TEXT_OUTPUT,
451 462 flags=0,
452 463 payload=payload)
453 464
454 465 class stream(object):
455 466 """Represents a logical unidirectional series of frames."""
456 467
457 468 def __init__(self, streamid, active=False):
458 469 self.streamid = streamid
459 470 self._active = active
460 471
461 472 def makeframe(self, requestid, typeid, flags, payload):
462 473 """Create a frame to be sent out over this stream.
463 474
464 475 Only returns the frame instance. Does not actually send it.
465 476 """
466 477 streamflags = 0
467 478 if not self._active:
468 479 streamflags |= STREAM_FLAG_BEGIN_STREAM
469 480 self._active = True
470 481
471 482 return makeframe(requestid, self.streamid, streamflags, typeid, flags,
472 483 payload)
473 484
474 485 def ensureserverstream(stream):
475 486 if stream.streamid % 2:
476 487 raise error.ProgrammingError('server should only write to even '
477 488 'numbered streams; %d is not even' %
478 489 stream.streamid)
479 490
480 491 class serverreactor(object):
481 492 """Holds state of a server handling frame-based protocol requests.
482 493
483 494 This class is the "brain" of the unified frame-based protocol server
484 495 component. While the protocol is stateless from the perspective of
485 496 requests/commands, something needs to track which frames have been
486 497 received, what frames to expect, etc. This class is that thing.
487 498
488 499 Instances are modeled as a state machine of sorts. Instances are also
489 500 reactionary to external events. The point of this class is to encapsulate
490 501 the state of the connection and the exchange of frames, not to perform
491 502 work. Instead, callers tell this class when something occurs, like a
492 503 frame arriving. If that activity is worthy of a follow-up action (say
493 504 *run a command*), the return value of that handler will say so.
494 505
495 506 I/O and CPU intensive operations are purposefully delegated outside of
496 507 this class.
497 508
498 509 Consumers are expected to tell instances when events occur. They do so by
499 510 calling the various ``on*`` methods. These methods return a 2-tuple
500 511 describing any follow-up action(s) to take. The first element is the
501 512 name of an action to perform. The second is a data structure (usually
502 513 a dict) specific to that action that contains more information. e.g.
503 514 if the server wants to send frames back to the client, the data structure
504 515 will contain a reference to those frames.
505 516
506 517 Valid actions that consumers can be instructed to take are:
507 518
508 519 sendframes
509 520 Indicates that frames should be sent to the client. The ``framegen``
510 521 key contains a generator of frames that should be sent. The server
511 522 assumes that all frames are sent to the client.
512 523
513 524 error
514 525 Indicates that an error occurred. Consumer should probably abort.
515 526
516 527 runcommand
517 528 Indicates that the consumer should run a wire protocol command. Details
518 529 of the command to run are given in the data structure.
519 530
520 531 wantframe
521 532 Indicates that nothing of interest happened and the server is waiting on
522 533 more frames from the client before anything interesting can be done.
523 534
524 535 noop
525 536 Indicates no additional action is required.
526 537
527 538 Known Issues
528 539 ------------
529 540
530 541 There are no limits to the number of partially received commands or their
531 542 size. A malicious client could stream command request data and exhaust the
532 543 server's memory.
533 544
534 545 Partially received commands are not acted upon when end of input is
535 546 reached. Should the server error if it receives a partial request?
536 547 Should the client send a message to abort a partially transmitted request
537 548 to facilitate graceful shutdown?
538 549
539 550 Active requests that haven't been responded to aren't tracked. This means
540 551 that if we receive a command and instruct its dispatch, another command
541 552 with its request ID can come in over the wire and there will be a race
542 553 between who responds to what.
543 554 """
544 555
545 556 def __init__(self, deferoutput=False):
546 557 """Construct a new server reactor.
547 558
548 559 ``deferoutput`` can be used to indicate that no output frames should be
549 560 instructed to be sent until input has been exhausted. In this mode,
550 561 events that would normally generate output frames (such as a command
551 562 response being ready) will instead defer instructing the consumer to
552 563 send those frames. This is useful for half-duplex transports where the
553 564 sender cannot receive until all data has been transmitted.
554 565 """
555 566 self._deferoutput = deferoutput
556 567 self._state = 'idle'
557 568 self._nextoutgoingstreamid = 2
558 569 self._bufferedframegens = []
559 570 # stream id -> stream instance for all active streams from the client.
560 571 self._incomingstreams = {}
561 572 self._outgoingstreams = {}
562 573 # request id -> dict of commands that are actively being received.
563 574 self._receivingcommands = {}
564 575 # Request IDs that have been received and are actively being processed.
565 576 # Once all output for a request has been sent, it is removed from this
566 577 # set.
567 578 self._activecommands = set()
568 579
569 580 def onframerecv(self, frame):
570 581 """Process a frame that has been received off the wire.
571 582
572 583 Returns a dict with an ``action`` key that details what action,
573 584 if any, the consumer should take next.
574 585 """
575 586 if not frame.streamid % 2:
576 587 self._state = 'errored'
577 588 return self._makeerrorresult(
578 589 _('received frame with even numbered stream ID: %d') %
579 590 frame.streamid)
580 591
581 592 if frame.streamid not in self._incomingstreams:
582 593 if not frame.streamflags & STREAM_FLAG_BEGIN_STREAM:
583 594 self._state = 'errored'
584 595 return self._makeerrorresult(
585 596 _('received frame on unknown inactive stream without '
586 597 'beginning of stream flag set'))
587 598
588 599 self._incomingstreams[frame.streamid] = stream(frame.streamid)
589 600
590 601 if frame.streamflags & STREAM_FLAG_ENCODING_APPLIED:
591 602 # TODO handle decoding frames
592 603 self._state = 'errored'
593 604 raise error.ProgrammingError('support for decoding stream payloads '
594 605 'not yet implemented')
595 606
596 607 if frame.streamflags & STREAM_FLAG_END_STREAM:
597 608 del self._incomingstreams[frame.streamid]
598 609
599 610 handlers = {
600 611 'idle': self._onframeidle,
601 612 'command-receiving': self._onframecommandreceiving,
602 613 'errored': self._onframeerrored,
603 614 }
604 615
605 616 meth = handlers.get(self._state)
606 617 if not meth:
607 618 raise error.ProgrammingError('unhandled state: %s' % self._state)
608 619
609 620 return meth(frame)
610 621
611 622 def oncommandresponseready(self, stream, requestid, data):
612 623 """Signal that a bytes response is ready to be sent to the client.
613 624
614 625 The raw bytes response is passed as an argument.
615 626 """
616 627 ensureserverstream(stream)
617 628
618 629 def sendframes():
619 630 for frame in createcommandresponseframesfrombytes(stream, requestid,
620 631 data):
621 632 yield frame
622 633
623 634 self._activecommands.remove(requestid)
624 635
625 636 result = sendframes()
626 637
627 638 if self._deferoutput:
628 639 self._bufferedframegens.append(result)
629 640 return 'noop', {}
630 641 else:
631 642 return 'sendframes', {
632 643 'framegen': result,
633 644 }
634 645
635 646 def oninputeof(self):
636 647 """Signals that end of input has been received.
637 648
638 649 No more frames will be received. All pending activity should be
639 650 completed.
640 651 """
641 652 # TODO should we do anything about in-flight commands?
642 653
643 654 if not self._deferoutput or not self._bufferedframegens:
644 655 return 'noop', {}
645 656
646 657 # If we buffered all our responses, emit those.
647 658 def makegen():
648 659 for gen in self._bufferedframegens:
649 660 for frame in gen:
650 661 yield frame
651 662
652 663 return 'sendframes', {
653 664 'framegen': makegen(),
654 665 }
655 666
656 667 def onapplicationerror(self, stream, requestid, msg):
657 668 ensureserverstream(stream)
658 669
659 670 return 'sendframes', {
660 671 'framegen': createerrorframe(stream, requestid, msg,
661 672 application=True),
662 673 }
663 674
664 675 def makeoutputstream(self):
665 676 """Create a stream to be used for sending data to the client."""
666 677 streamid = self._nextoutgoingstreamid
667 678 self._nextoutgoingstreamid += 2
668 679
669 680 s = stream(streamid)
670 681 self._outgoingstreams[streamid] = s
671 682
672 683 return s
673 684
674 685 def _makeerrorresult(self, msg):
675 686 return 'error', {
676 687 'message': msg,
677 688 }
678 689
679 690 def _makeruncommandresult(self, requestid):
680 691 entry = self._receivingcommands[requestid]
681 692
682 693 if not entry['requestdone']:
683 694 self._state = 'errored'
684 695 raise error.ProgrammingError('should not be called without '
685 696 'requestdone set')
686 697
687 698 del self._receivingcommands[requestid]
688 699
689 700 if self._receivingcommands:
690 701 self._state = 'command-receiving'
691 702 else:
692 703 self._state = 'idle'
693 704
694 705 # Decode the payloads as CBOR.
695 706 entry['payload'].seek(0)
696 707 request = cbor.load(entry['payload'])
697 708
698 709 if b'name' not in request:
699 710 self._state = 'errored'
700 711 return self._makeerrorresult(
701 712 _('command request missing "name" field'))
702 713
703 714 if b'args' not in request:
704 715 request[b'args'] = {}
705 716
706 717 assert requestid not in self._activecommands
707 718 self._activecommands.add(requestid)
708 719
709 720 return 'runcommand', {
710 721 'requestid': requestid,
711 722 'command': request[b'name'],
712 723 'args': request[b'args'],
713 724 'data': entry['data'].getvalue() if entry['data'] else None,
714 725 }
715 726
716 727 def _makewantframeresult(self):
717 728 return 'wantframe', {
718 729 'state': self._state,
719 730 }
720 731
721 732 def _validatecommandrequestframe(self, frame):
722 733 new = frame.flags & FLAG_COMMAND_REQUEST_NEW
723 734 continuation = frame.flags & FLAG_COMMAND_REQUEST_CONTINUATION
724 735
725 736 if new and continuation:
726 737 self._state = 'errored'
727 738 return self._makeerrorresult(
728 739 _('received command request frame with both new and '
729 740 'continuation flags set'))
730 741
731 742 if not new and not continuation:
732 743 self._state = 'errored'
733 744 return self._makeerrorresult(
734 745 _('received command request frame with neither new nor '
735 746 'continuation flags set'))
736 747
737 748 def _onframeidle(self, frame):
738 749 # The only frame type that should be received in this state is a
739 750 # command request.
740 751 if frame.typeid != FRAME_TYPE_COMMAND_REQUEST:
741 752 self._state = 'errored'
742 753 return self._makeerrorresult(
743 754 _('expected command request frame; got %d') % frame.typeid)
744 755
745 756 res = self._validatecommandrequestframe(frame)
746 757 if res:
747 758 return res
748 759
749 760 if frame.requestid in self._receivingcommands:
750 761 self._state = 'errored'
751 762 return self._makeerrorresult(
752 763 _('request with ID %d already received') % frame.requestid)
753 764
754 765 if frame.requestid in self._activecommands:
755 766 self._state = 'errored'
756 767 return self._makeerrorresult(
757 768 _('request with ID %d is already active') % frame.requestid)
758 769
759 770 new = frame.flags & FLAG_COMMAND_REQUEST_NEW
760 771 moreframes = frame.flags & FLAG_COMMAND_REQUEST_MORE_FRAMES
761 772 expectingdata = frame.flags & FLAG_COMMAND_REQUEST_EXPECT_DATA
762 773
763 774 if not new:
764 775 self._state = 'errored'
765 776 return self._makeerrorresult(
766 777 _('received command request frame without new flag set'))
767 778
768 779 payload = util.bytesio()
769 780 payload.write(frame.payload)
770 781
771 782 self._receivingcommands[frame.requestid] = {
772 783 'payload': payload,
773 784 'data': None,
774 785 'requestdone': not moreframes,
775 786 'expectingdata': bool(expectingdata),
776 787 }
777 788
778 789 # This is the final frame for this request. Dispatch it.
779 790 if not moreframes and not expectingdata:
780 791 return self._makeruncommandresult(frame.requestid)
781 792
782 793 assert moreframes or expectingdata
783 794 self._state = 'command-receiving'
784 795 return self._makewantframeresult()
785 796
786 797 def _onframecommandreceiving(self, frame):
787 798 if frame.typeid == FRAME_TYPE_COMMAND_REQUEST:
788 799 # Process new command requests as such.
789 800 if frame.flags & FLAG_COMMAND_REQUEST_NEW:
790 801 return self._onframeidle(frame)
791 802
792 803 res = self._validatecommandrequestframe(frame)
793 804 if res:
794 805 return res
795 806
796 807 # All other frames should be related to a command that is currently
797 808 # receiving but is not active.
798 809 if frame.requestid in self._activecommands:
799 810 self._state = 'errored'
800 811 return self._makeerrorresult(
801 812 _('received frame for request that is still active: %d') %
802 813 frame.requestid)
803 814
804 815 if frame.requestid not in self._receivingcommands:
805 816 self._state = 'errored'
806 817 return self._makeerrorresult(
807 818 _('received frame for request that is not receiving: %d') %
808 819 frame.requestid)
809 820
810 821 entry = self._receivingcommands[frame.requestid]
811 822
812 823 if frame.typeid == FRAME_TYPE_COMMAND_REQUEST:
813 824 moreframes = frame.flags & FLAG_COMMAND_REQUEST_MORE_FRAMES
814 825 expectingdata = bool(frame.flags & FLAG_COMMAND_REQUEST_EXPECT_DATA)
815 826
816 827 if entry['requestdone']:
817 828 self._state = 'errored'
818 829 return self._makeerrorresult(
819 830 _('received command request frame when request frames '
820 831 'were supposedly done'))
821 832
822 833 if expectingdata != entry['expectingdata']:
823 834 self._state = 'errored'
824 835 return self._makeerrorresult(
825 836 _('mismatch between expect data flag and previous frame'))
826 837
827 838 entry['payload'].write(frame.payload)
828 839
829 840 if not moreframes:
830 841 entry['requestdone'] = True
831 842
832 843 if not moreframes and not expectingdata:
833 844 return self._makeruncommandresult(frame.requestid)
834 845
835 846 return self._makewantframeresult()
836 847
837 848 elif frame.typeid == FRAME_TYPE_COMMAND_DATA:
838 849 if not entry['expectingdata']:
839 850 self._state = 'errored'
840 851 return self._makeerrorresult(_(
841 852 'received command data frame for request that is not '
842 853 'expecting data: %d') % frame.requestid)
843 854
844 855 if entry['data'] is None:
845 856 entry['data'] = util.bytesio()
846 857
847 858 return self._handlecommanddataframe(frame, entry)
848 859 else:
849 860 self._state = 'errored'
850 861 return self._makeerrorresult(_(
851 862 'received unexpected frame type: %d') % frame.typeid)
852 863
853 864 def _handlecommanddataframe(self, frame, entry):
854 865 assert frame.typeid == FRAME_TYPE_COMMAND_DATA
855 866
856 867 # TODO support streaming data instead of buffering it.
857 868 entry['data'].write(frame.payload)
858 869
859 870 if frame.flags & FLAG_COMMAND_DATA_CONTINUATION:
860 871 return self._makewantframeresult()
861 872 elif frame.flags & FLAG_COMMAND_DATA_EOS:
862 873 entry['data'].seek(0)
863 874 return self._makeruncommandresult(frame.requestid)
864 875 else:
865 876 self._state = 'errored'
866 877 return self._makeerrorresult(_('command data frame without '
867 878 'flags'))
868 879
869 880 def _onframeerrored(self, frame):
870 881 return self._makeerrorresult(_('server already errored'))
871 882
872 883 class commandrequest(object):
873 884 """Represents a request to run a command."""
874 885
875 886 def __init__(self, requestid, name, args, datafh=None):
876 887 self.requestid = requestid
877 888 self.name = name
878 889 self.args = args
879 890 self.datafh = datafh
880 891 self.state = 'pending'
881 892
882 893 class clientreactor(object):
883 894 """Holds state of a client issuing frame-based protocol requests.
884 895
885 896 This is like ``serverreactor`` but for client-side state.
886 897
887 898 Each instance is bound to the lifetime of a connection. For persistent
888 899 connection transports using e.g. TCP sockets and speaking the raw
889 900 framing protocol, there will be a single instance for the lifetime of
890 901 the TCP socket. For transports where there are multiple discrete
891 902 interactions (say tunneled within in HTTP request), there will be a
892 903 separate instance for each distinct interaction.
893 904 """
894 905 def __init__(self, hasmultiplesend=False, buffersends=True):
895 906 """Create a new instance.
896 907
897 908 ``hasmultiplesend`` indicates whether multiple sends are supported
898 909 by the transport. When True, it is possible to send commands immediately
899 910 instead of buffering until the caller signals an intent to finish a
900 911 send operation.
901 912
902 913 ``buffercommands`` indicates whether sends should be buffered until the
903 914 last request has been issued.
904 915 """
905 916 self._hasmultiplesend = hasmultiplesend
906 917 self._buffersends = buffersends
907 918
908 919 self._canissuecommands = True
909 920 self._cansend = True
910 921
911 922 self._nextrequestid = 1
912 923 # We only support a single outgoing stream for now.
913 924 self._outgoingstream = stream(1)
914 925 self._pendingrequests = collections.deque()
915 926 self._activerequests = {}
916 927 self._incomingstreams = {}
917 928
918 929 def callcommand(self, name, args, datafh=None):
919 930 """Request that a command be executed.
920 931
921 932 Receives the command name, a dict of arguments to pass to the command,
922 933 and an optional file object containing the raw data for the command.
923 934
924 935 Returns a 3-tuple of (request, action, action data).
925 936 """
926 937 if not self._canissuecommands:
927 938 raise error.ProgrammingError('cannot issue new commands')
928 939
929 940 requestid = self._nextrequestid
930 941 self._nextrequestid += 2
931 942
932 943 request = commandrequest(requestid, name, args, datafh=datafh)
933 944
934 945 if self._buffersends:
935 946 self._pendingrequests.append(request)
936 947 return request, 'noop', {}
937 948 else:
938 949 if not self._cansend:
939 950 raise error.ProgrammingError('sends cannot be performed on '
940 951 'this instance')
941 952
942 953 if not self._hasmultiplesend:
943 954 self._cansend = False
944 955 self._canissuecommands = False
945 956
946 957 return request, 'sendframes', {
947 958 'framegen': self._makecommandframes(request),
948 959 }
949 960
950 961 def flushcommands(self):
951 962 """Request that all queued commands be sent.
952 963
953 964 If any commands are buffered, this will instruct the caller to send
954 965 them over the wire. If no commands are buffered it instructs the client
955 966 to no-op.
956 967
957 968 If instances aren't configured for multiple sends, no new command
958 969 requests are allowed after this is called.
959 970 """
960 971 if not self._pendingrequests:
961 972 return 'noop', {}
962 973
963 974 if not self._cansend:
964 975 raise error.ProgrammingError('sends cannot be performed on this '
965 976 'instance')
966 977
967 978 # If the instance only allows sending once, mark that we have fired
968 979 # our one shot.
969 980 if not self._hasmultiplesend:
970 981 self._canissuecommands = False
971 982 self._cansend = False
972 983
973 984 def makeframes():
974 985 while self._pendingrequests:
975 986 request = self._pendingrequests.popleft()
976 987 for frame in self._makecommandframes(request):
977 988 yield frame
978 989
979 990 return 'sendframes', {
980 991 'framegen': makeframes(),
981 992 }
982 993
983 994 def _makecommandframes(self, request):
984 995 """Emit frames to issue a command request.
985 996
986 997 As a side-effect, update request accounting to reflect its changed
987 998 state.
988 999 """
989 1000 self._activerequests[request.requestid] = request
990 1001 request.state = 'sending'
991 1002
992 1003 res = createcommandframes(self._outgoingstream,
993 1004 request.requestid,
994 1005 request.name,
995 1006 request.args,
996 1007 request.datafh)
997 1008
998 1009 for frame in res:
999 1010 yield frame
1000 1011
1001 1012 request.state = 'sent'
1002 1013
1003 1014 def onframerecv(self, frame):
1004 1015 """Process a frame that has been received off the wire.
1005 1016
1006 1017 Returns a 2-tuple of (action, meta) describing further action the
1007 1018 caller needs to take as a result of receiving this frame.
1008 1019 """
1009 1020 if frame.streamid % 2:
1010 1021 return 'error', {
1011 1022 'message': (
1012 1023 _('received frame with odd numbered stream ID: %d') %
1013 1024 frame.streamid),
1014 1025 }
1015 1026
1016 1027 if frame.streamid not in self._incomingstreams:
1017 1028 if not frame.streamflags & STREAM_FLAG_BEGIN_STREAM:
1018 1029 return 'error', {
1019 1030 'message': _('received frame on unknown stream '
1020 1031 'without beginning of stream flag set'),
1021 1032 }
1022 1033
1023 1034 self._incomingstreams[frame.streamid] = stream(frame.streamid)
1024 1035
1025 1036 if frame.streamflags & STREAM_FLAG_ENCODING_APPLIED:
1026 1037 raise error.ProgrammingError('support for decoding stream '
1027 1038 'payloads not yet implemneted')
1028 1039
1029 1040 if frame.streamflags & STREAM_FLAG_END_STREAM:
1030 1041 del self._incomingstreams[frame.streamid]
1031 1042
1032 1043 if frame.requestid not in self._activerequests:
1033 1044 return 'error', {
1034 1045 'message': (_('received frame for inactive request ID: %d') %
1035 1046 frame.requestid),
1036 1047 }
1037 1048
1038 1049 request = self._activerequests[frame.requestid]
1039 1050 request.state = 'receiving'
1040 1051
1041 1052 handlers = {
1042 1053 FRAME_TYPE_COMMAND_RESPONSE: self._oncommandresponseframe,
1043 1054 }
1044 1055
1045 1056 meth = handlers.get(frame.typeid)
1046 1057 if not meth:
1047 1058 raise error.ProgrammingError('unhandled frame type: %d' %
1048 1059 frame.typeid)
1049 1060
1050 1061 return meth(request, frame)
1051 1062
1052 1063 def _oncommandresponseframe(self, request, frame):
1053 1064 if frame.flags & FLAG_COMMAND_RESPONSE_EOS:
1054 1065 request.state = 'received'
1055 1066 del self._activerequests[request.requestid]
1056 1067
1057 1068 return 'responsedata', {
1058 1069 'request': request,
1059 1070 'expectmore': frame.flags & FLAG_COMMAND_RESPONSE_CONTINUATION,
1060 1071 'eos': frame.flags & FLAG_COMMAND_RESPONSE_EOS,
1061 1072 'data': frame.payload,
1062 1073 }
@@ -1,175 +1,201 b''
1 1 # wireprotov2peer.py - client side code for wire protocol version 2
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 .i18n import _
11 11 from .thirdparty import (
12 12 cbor,
13 13 )
14 14 from . import (
15 15 encoding,
16 16 error,
17 17 util,
18 18 wireprotoframing,
19 19 )
20 20
21 def formatrichmessage(atoms):
22 """Format an encoded message from the framing protocol."""
23
24 chunks = []
25
26 for atom in atoms:
27 msg = _(atom[b'msg'])
28
29 if b'args' in atom:
30 msg = msg % atom[b'args']
31
32 chunks.append(msg)
33
34 return b''.join(chunks)
35
21 36 class commandresponse(object):
22 37 """Represents the response to a command request."""
23 38
24 39 def __init__(self, requestid, command):
25 40 self.requestid = requestid
26 41 self.command = command
27 42
28 43 self.b = util.bytesio()
29 44
30 45 def cborobjects(self):
31 46 """Obtain decoded CBOR objects from this response."""
32 47 size = self.b.tell()
33 48 self.b.seek(0)
34 49
35 50 decoder = cbor.CBORDecoder(self.b)
36 51
37 52 while self.b.tell() < size:
38 53 yield decoder.decode()
39 54
40 55 class clienthandler(object):
41 56 """Object to handle higher-level client activities.
42 57
43 58 The ``clientreactor`` is used to hold low-level state about the frame-based
44 59 protocol, such as which requests and streams are active. This type is used
45 60 for higher-level operations, such as reading frames from a socket, exposing
46 61 and managing a higher-level primitive for representing command responses,
47 62 etc. This class is what peers should probably use to bridge wire activity
48 63 with the higher-level peer API.
49 64 """
50 65
51 66 def __init__(self, ui, clientreactor):
52 67 self._ui = ui
53 68 self._reactor = clientreactor
54 69 self._requests = {}
55 70 self._futures = {}
56 71 self._responses = {}
57 72
58 73 def callcommand(self, command, args, f):
59 74 """Register a request to call a command.
60 75
61 76 Returns an iterable of frames that should be sent over the wire.
62 77 """
63 78 request, action, meta = self._reactor.callcommand(command, args)
64 79
65 80 if action != 'noop':
66 81 raise error.ProgrammingError('%s not yet supported' % action)
67 82
68 83 rid = request.requestid
69 84 self._requests[rid] = request
70 85 self._futures[rid] = f
71 86 self._responses[rid] = commandresponse(rid, command)
72 87
73 88 return iter(())
74 89
75 90 def flushcommands(self):
76 91 """Flush all queued commands.
77 92
78 93 Returns an iterable of frames that should be sent over the wire.
79 94 """
80 95 action, meta = self._reactor.flushcommands()
81 96
82 97 if action != 'sendframes':
83 98 raise error.ProgrammingError('%s not yet supported' % action)
84 99
85 100 return meta['framegen']
86 101
87 102 def readframe(self, fh):
88 103 """Attempt to read and process a frame.
89 104
90 105 Returns None if no frame was read. Presumably this means EOF.
91 106 """
92 107 frame = wireprotoframing.readframe(fh)
93 108 if frame is None:
94 109 # TODO tell reactor?
95 110 return
96 111
97 112 self._ui.note(_('received %r\n') % frame)
98 113 self._processframe(frame)
99 114
100 115 return True
101 116
102 117 def _processframe(self, frame):
103 118 """Process a single read frame."""
104 119
105 120 action, meta = self._reactor.onframerecv(frame)
106 121
107 122 if action == 'error':
108 123 e = error.RepoError(meta['message'])
109 124
110 125 if frame.requestid in self._futures:
111 126 self._futures[frame.requestid].set_exception(e)
112 127 else:
113 128 raise e
114 129
115 130 if frame.requestid not in self._requests:
116 131 raise error.ProgrammingError(
117 132 'received frame for unknown request; this is either a bug in '
118 133 'the clientreactor not screening for this or this instance was '
119 134 'never told about this request: %r' % frame)
120 135
121 136 response = self._responses[frame.requestid]
122 137
123 138 if action == 'responsedata':
124 139 response.b.write(meta['data'])
125 140
126 141 if meta['eos']:
127 142 # If the command has a decoder, resolve the future to the
128 143 # decoded value. Otherwise resolve to the rich response object.
129 144 decoder = COMMAND_DECODERS.get(response.command)
130 145
131 result = decoder(response) if decoder else response
146 # TODO consider always resolving the overall status map.
147 if decoder:
148 objs = response.cborobjects()
149
150 overall = next(objs)
132 151
133 self._futures[frame.requestid].set_result(result)
152 if overall['status'] == 'ok':
153 self._futures[frame.requestid].set_result(decoder(objs))
154 else:
155 e = error.RepoError(
156 formatrichmessage(overall['error']['message']))
157 self._futures[frame.requestid].set_exception(e)
158 else:
159 self._futures[frame.requestid].set_result(response)
134 160
135 161 del self._requests[frame.requestid]
136 162 del self._futures[frame.requestid]
137 163
138 164 else:
139 165 raise error.ProgrammingError(
140 166 'unhandled action from clientreactor: %s' % action)
141 167
142 def decodebranchmap(resp):
168 def decodebranchmap(objs):
143 169 # Response should be a single CBOR map of branch name to array of nodes.
144 bm = next(resp.cborobjects())
170 bm = next(objs)
145 171
146 172 return {encoding.tolocal(k): v for k, v in bm.items()}
147 173
148 def decodeheads(resp):
174 def decodeheads(objs):
149 175 # Array of node bytestrings.
150 return next(resp.cborobjects())
176 return next(objs)
151 177
152 def decodeknown(resp):
178 def decodeknown(objs):
153 179 # Bytestring where each byte is a 0 or 1.
154 raw = next(resp.cborobjects())
180 raw = next(objs)
155 181
156 182 return [True if c == '1' else False for c in raw]
157 183
158 def decodelistkeys(resp):
184 def decodelistkeys(objs):
159 185 # Map with bytestring keys and values.
160 return next(resp.cborobjects())
186 return next(objs)
161 187
162 def decodelookup(resp):
163 return next(resp.cborobjects())
188 def decodelookup(objs):
189 return next(objs)
164 190
165 def decodepushkey(resp):
166 return next(resp.cborobjects())
191 def decodepushkey(objs):
192 return next(objs)
167 193
168 194 COMMAND_DECODERS = {
169 195 'branchmap': decodebranchmap,
170 196 'heads': decodeheads,
171 197 'known': decodeknown,
172 198 'listkeys': decodelistkeys,
173 199 'lookup': decodelookup,
174 200 'pushkey': decodepushkey,
175 201 }
@@ -1,484 +1,484 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
11 11 from .i18n import _
12 12 from .thirdparty import (
13 13 cbor,
14 14 )
15 15 from .thirdparty.zope import (
16 16 interface as zi,
17 17 )
18 18 from . import (
19 19 encoding,
20 20 error,
21 21 pycompat,
22 22 streamclone,
23 23 util,
24 24 wireproto,
25 25 wireprotoframing,
26 26 wireprototypes,
27 27 )
28 28
29 FRAMINGTYPE = b'application/mercurial-exp-framing-0004'
29 FRAMINGTYPE = b'application/mercurial-exp-framing-0005'
30 30
31 31 HTTP_WIREPROTO_V2 = wireprototypes.HTTP_WIREPROTO_V2
32 32
33 33 def handlehttpv2request(rctx, req, res, checkperm, urlparts):
34 34 from .hgweb import common as hgwebcommon
35 35
36 36 # URL space looks like: <permissions>/<command>, where <permission> can
37 37 # be ``ro`` or ``rw`` to signal read-only or read-write, respectively.
38 38
39 39 # Root URL does nothing meaningful... yet.
40 40 if not urlparts:
41 41 res.status = b'200 OK'
42 42 res.headers[b'Content-Type'] = b'text/plain'
43 43 res.setbodybytes(_('HTTP version 2 API handler'))
44 44 return
45 45
46 46 if len(urlparts) == 1:
47 47 res.status = b'404 Not Found'
48 48 res.headers[b'Content-Type'] = b'text/plain'
49 49 res.setbodybytes(_('do not know how to process %s\n') %
50 50 req.dispatchpath)
51 51 return
52 52
53 53 permission, command = urlparts[0:2]
54 54
55 55 if permission not in (b'ro', b'rw'):
56 56 res.status = b'404 Not Found'
57 57 res.headers[b'Content-Type'] = b'text/plain'
58 58 res.setbodybytes(_('unknown permission: %s') % permission)
59 59 return
60 60
61 61 if req.method != 'POST':
62 62 res.status = b'405 Method Not Allowed'
63 63 res.headers[b'Allow'] = b'POST'
64 64 res.setbodybytes(_('commands require POST requests'))
65 65 return
66 66
67 67 # At some point we'll want to use our own API instead of recycling the
68 68 # behavior of version 1 of the wire protocol...
69 69 # TODO return reasonable responses - not responses that overload the
70 70 # HTTP status line message for error reporting.
71 71 try:
72 72 checkperm(rctx, req, 'pull' if permission == b'ro' else 'push')
73 73 except hgwebcommon.ErrorResponse as e:
74 74 res.status = hgwebcommon.statusmessage(e.code, pycompat.bytestr(e))
75 75 for k, v in e.headers:
76 76 res.headers[k] = v
77 77 res.setbodybytes('permission denied')
78 78 return
79 79
80 80 # We have a special endpoint to reflect the request back at the client.
81 81 if command == b'debugreflect':
82 82 _processhttpv2reflectrequest(rctx.repo.ui, rctx.repo, req, res)
83 83 return
84 84
85 85 # Extra commands that we handle that aren't really wire protocol
86 86 # commands. Think extra hard before making this hackery available to
87 87 # extension.
88 88 extracommands = {'multirequest'}
89 89
90 90 if command not in wireproto.commandsv2 and command not in extracommands:
91 91 res.status = b'404 Not Found'
92 92 res.headers[b'Content-Type'] = b'text/plain'
93 93 res.setbodybytes(_('unknown wire protocol command: %s\n') % command)
94 94 return
95 95
96 96 repo = rctx.repo
97 97 ui = repo.ui
98 98
99 99 proto = httpv2protocolhandler(req, ui)
100 100
101 101 if (not wireproto.commandsv2.commandavailable(command, proto)
102 102 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(_('invalid wire protocol command: %s') % command)
106 106 return
107 107
108 108 # TODO consider cases where proxies may add additional Accept headers.
109 109 if req.headers.get(b'Accept') != FRAMINGTYPE:
110 110 res.status = b'406 Not Acceptable'
111 111 res.headers[b'Content-Type'] = b'text/plain'
112 112 res.setbodybytes(_('client MUST specify Accept header with value: %s\n')
113 113 % FRAMINGTYPE)
114 114 return
115 115
116 116 if req.headers.get(b'Content-Type') != FRAMINGTYPE:
117 117 res.status = b'415 Unsupported Media Type'
118 118 # TODO we should send a response with appropriate media type,
119 119 # since client does Accept it.
120 120 res.headers[b'Content-Type'] = b'text/plain'
121 121 res.setbodybytes(_('client MUST send Content-Type header with '
122 122 'value: %s\n') % FRAMINGTYPE)
123 123 return
124 124
125 125 _processhttpv2request(ui, repo, req, res, permission, command, proto)
126 126
127 127 def _processhttpv2reflectrequest(ui, repo, req, res):
128 128 """Reads unified frame protocol request and dumps out state to client.
129 129
130 130 This special endpoint can be used to help debug the wire protocol.
131 131
132 132 Instead of routing the request through the normal dispatch mechanism,
133 133 we instead read all frames, decode them, and feed them into our state
134 134 tracker. We then dump the log of all that activity back out to the
135 135 client.
136 136 """
137 137 import json
138 138
139 139 # Reflection APIs have a history of being abused, accidentally disclosing
140 140 # sensitive data, etc. So we have a config knob.
141 141 if not ui.configbool('experimental', 'web.api.debugreflect'):
142 142 res.status = b'404 Not Found'
143 143 res.headers[b'Content-Type'] = b'text/plain'
144 144 res.setbodybytes(_('debugreflect service not available'))
145 145 return
146 146
147 147 # We assume we have a unified framing protocol request body.
148 148
149 149 reactor = wireprotoframing.serverreactor()
150 150 states = []
151 151
152 152 while True:
153 153 frame = wireprotoframing.readframe(req.bodyfh)
154 154
155 155 if not frame:
156 156 states.append(b'received: <no frame>')
157 157 break
158 158
159 159 states.append(b'received: %d %d %d %s' % (frame.typeid, frame.flags,
160 160 frame.requestid,
161 161 frame.payload))
162 162
163 163 action, meta = reactor.onframerecv(frame)
164 164 states.append(json.dumps((action, meta), sort_keys=True,
165 165 separators=(', ', ': ')))
166 166
167 167 action, meta = reactor.oninputeof()
168 168 meta['action'] = action
169 169 states.append(json.dumps(meta, sort_keys=True, separators=(', ',': ')))
170 170
171 171 res.status = b'200 OK'
172 172 res.headers[b'Content-Type'] = b'text/plain'
173 173 res.setbodybytes(b'\n'.join(states))
174 174
175 175 def _processhttpv2request(ui, repo, req, res, authedperm, reqcommand, proto):
176 176 """Post-validation handler for HTTPv2 requests.
177 177
178 178 Called when the HTTP request contains unified frame-based protocol
179 179 frames for evaluation.
180 180 """
181 181 # TODO Some HTTP clients are full duplex and can receive data before
182 182 # the entire request is transmitted. Figure out a way to indicate support
183 183 # for that so we can opt into full duplex mode.
184 184 reactor = wireprotoframing.serverreactor(deferoutput=True)
185 185 seencommand = False
186 186
187 187 outstream = reactor.makeoutputstream()
188 188
189 189 while True:
190 190 frame = wireprotoframing.readframe(req.bodyfh)
191 191 if not frame:
192 192 break
193 193
194 194 action, meta = reactor.onframerecv(frame)
195 195
196 196 if action == 'wantframe':
197 197 # Need more data before we can do anything.
198 198 continue
199 199 elif action == 'runcommand':
200 200 sentoutput = _httpv2runcommand(ui, repo, req, res, authedperm,
201 201 reqcommand, reactor, outstream,
202 202 meta, issubsequent=seencommand)
203 203
204 204 if sentoutput:
205 205 return
206 206
207 207 seencommand = True
208 208
209 209 elif action == 'error':
210 210 # TODO define proper error mechanism.
211 211 res.status = b'200 OK'
212 212 res.headers[b'Content-Type'] = b'text/plain'
213 213 res.setbodybytes(meta['message'] + b'\n')
214 214 return
215 215 else:
216 216 raise error.ProgrammingError(
217 217 'unhandled action from frame processor: %s' % action)
218 218
219 219 action, meta = reactor.oninputeof()
220 220 if action == 'sendframes':
221 221 # We assume we haven't started sending the response yet. If we're
222 222 # wrong, the response type will raise an exception.
223 223 res.status = b'200 OK'
224 224 res.headers[b'Content-Type'] = FRAMINGTYPE
225 225 res.setbodygen(meta['framegen'])
226 226 elif action == 'noop':
227 227 pass
228 228 else:
229 229 raise error.ProgrammingError('unhandled action from frame processor: %s'
230 230 % action)
231 231
232 232 def _httpv2runcommand(ui, repo, req, res, authedperm, reqcommand, reactor,
233 233 outstream, command, issubsequent):
234 234 """Dispatch a wire protocol command made from HTTPv2 requests.
235 235
236 236 The authenticated permission (``authedperm``) along with the original
237 237 command from the URL (``reqcommand``) are passed in.
238 238 """
239 239 # We already validated that the session has permissions to perform the
240 240 # actions in ``authedperm``. In the unified frame protocol, the canonical
241 241 # command to run is expressed in a frame. However, the URL also requested
242 242 # to run a specific command. We need to be careful that the command we
243 243 # run doesn't have permissions requirements greater than what was granted
244 244 # by ``authedperm``.
245 245 #
246 246 # Our rule for this is we only allow one command per HTTP request and
247 247 # that command must match the command in the URL. However, we make
248 248 # an exception for the ``multirequest`` URL. This URL is allowed to
249 249 # execute multiple commands. We double check permissions of each command
250 250 # as it is invoked to ensure there is no privilege escalation.
251 251 # TODO consider allowing multiple commands to regular command URLs
252 252 # iff each command is the same.
253 253
254 254 proto = httpv2protocolhandler(req, ui, args=command['args'])
255 255
256 256 if reqcommand == b'multirequest':
257 257 if not wireproto.commandsv2.commandavailable(command['command'], proto):
258 258 # TODO proper error mechanism
259 259 res.status = b'200 OK'
260 260 res.headers[b'Content-Type'] = b'text/plain'
261 261 res.setbodybytes(_('wire protocol command not available: %s') %
262 262 command['command'])
263 263 return True
264 264
265 265 # TODO don't use assert here, since it may be elided by -O.
266 266 assert authedperm in (b'ro', b'rw')
267 267 wirecommand = wireproto.commandsv2[command['command']]
268 268 assert wirecommand.permission in ('push', 'pull')
269 269
270 270 if authedperm == b'ro' and wirecommand.permission != 'pull':
271 271 # TODO proper error mechanism
272 272 res.status = b'403 Forbidden'
273 273 res.headers[b'Content-Type'] = b'text/plain'
274 274 res.setbodybytes(_('insufficient permissions to execute '
275 275 'command: %s') % command['command'])
276 276 return True
277 277
278 278 # TODO should we also call checkperm() here? Maybe not if we're going
279 279 # to overhaul that API. The granted scope from the URL check should
280 280 # be good enough.
281 281
282 282 else:
283 283 # Don't allow multiple commands outside of ``multirequest`` URL.
284 284 if issubsequent:
285 285 # TODO proper error mechanism
286 286 res.status = b'200 OK'
287 287 res.headers[b'Content-Type'] = b'text/plain'
288 288 res.setbodybytes(_('multiple commands cannot be issued to this '
289 289 'URL'))
290 290 return True
291 291
292 292 if reqcommand != command['command']:
293 293 # TODO define proper error mechanism
294 294 res.status = b'200 OK'
295 295 res.headers[b'Content-Type'] = b'text/plain'
296 296 res.setbodybytes(_('command in frame must match command in URL'))
297 297 return True
298 298
299 299 rsp = wireproto.dispatch(repo, proto, command['command'])
300 300
301 301 res.status = b'200 OK'
302 302 res.headers[b'Content-Type'] = FRAMINGTYPE
303 303
304 304 if isinstance(rsp, wireprototypes.bytesresponse):
305 305 action, meta = reactor.oncommandresponseready(outstream,
306 306 command['requestid'],
307 307 rsp.data)
308 308 elif isinstance(rsp, wireprototypes.cborresponse):
309 309 encoded = cbor.dumps(rsp.value, canonical=True)
310 310 action, meta = reactor.oncommandresponseready(outstream,
311 311 command['requestid'],
312 312 encoded)
313 313 else:
314 314 action, meta = reactor.onapplicationerror(
315 315 _('unhandled response type from wire proto command'))
316 316
317 317 if action == 'sendframes':
318 318 res.setbodygen(meta['framegen'])
319 319 return True
320 320 elif action == 'noop':
321 321 return False
322 322 else:
323 323 raise error.ProgrammingError('unhandled event from reactor: %s' %
324 324 action)
325 325
326 326 @zi.implementer(wireprototypes.baseprotocolhandler)
327 327 class httpv2protocolhandler(object):
328 328 def __init__(self, req, ui, args=None):
329 329 self._req = req
330 330 self._ui = ui
331 331 self._args = args
332 332
333 333 @property
334 334 def name(self):
335 335 return HTTP_WIREPROTO_V2
336 336
337 337 def getargs(self, args):
338 338 data = {}
339 339 for k, typ in args.items():
340 340 if k == '*':
341 341 raise NotImplementedError('do not support * args')
342 342 elif k in self._args:
343 343 # TODO consider validating value types.
344 344 data[k] = self._args[k]
345 345
346 346 return data
347 347
348 348 def getprotocaps(self):
349 349 # Protocol capabilities are currently not implemented for HTTP V2.
350 350 return set()
351 351
352 352 def getpayload(self):
353 353 raise NotImplementedError
354 354
355 355 @contextlib.contextmanager
356 356 def mayberedirectstdio(self):
357 357 raise NotImplementedError
358 358
359 359 def client(self):
360 360 raise NotImplementedError
361 361
362 362 def addcapabilities(self, repo, caps):
363 363 return caps
364 364
365 365 def checkperm(self, perm):
366 366 raise NotImplementedError
367 367
368 368 def httpv2apidescriptor(req, repo):
369 369 proto = httpv2protocolhandler(req, repo.ui)
370 370
371 371 return _capabilitiesv2(repo, proto)
372 372
373 373 def _capabilitiesv2(repo, proto):
374 374 """Obtain the set of capabilities for version 2 transports.
375 375
376 376 These capabilities are distinct from the capabilities for version 1
377 377 transports.
378 378 """
379 379 compression = []
380 380 for engine in wireproto.supportedcompengines(repo.ui, util.SERVERROLE):
381 381 compression.append({
382 382 b'name': engine.wireprotosupport().name,
383 383 })
384 384
385 385 caps = {
386 386 'commands': {},
387 387 'compression': compression,
388 388 'framingmediatypes': [FRAMINGTYPE],
389 389 }
390 390
391 391 for command, entry in wireproto.commandsv2.items():
392 392 caps['commands'][command] = {
393 393 'args': entry.args,
394 394 'permissions': [entry.permission],
395 395 }
396 396
397 397 if streamclone.allowservergeneration(repo):
398 398 caps['rawrepoformats'] = sorted(repo.requirements &
399 399 repo.supportedformats)
400 400
401 401 return proto.addcapabilities(repo, caps)
402 402
403 403 def wireprotocommand(*args, **kwargs):
404 404 def register(func):
405 405 return wireproto.wireprotocommand(
406 406 *args, transportpolicy=wireproto.POLICY_V2_ONLY, **kwargs)(func)
407 407
408 408 return register
409 409
410 410 @wireprotocommand('branchmap', permission='pull')
411 411 def branchmapv2(repo, proto):
412 412 branchmap = {encoding.fromlocal(k): v
413 413 for k, v in repo.branchmap().iteritems()}
414 414
415 415 return wireprototypes.cborresponse(branchmap)
416 416
417 417 @wireprotocommand('capabilities', permission='pull')
418 418 def capabilitiesv2(repo, proto):
419 419 caps = _capabilitiesv2(repo, proto)
420 420
421 421 return wireprototypes.cborresponse(caps)
422 422
423 423 @wireprotocommand('heads',
424 424 args={
425 425 'publiconly': False,
426 426 },
427 427 permission='pull')
428 428 def headsv2(repo, proto, publiconly=False):
429 429 if publiconly:
430 430 repo = repo.filtered('immutable')
431 431
432 432 return wireprototypes.cborresponse(repo.heads())
433 433
434 434 @wireprotocommand('known',
435 435 args={
436 436 'nodes': [b'deadbeef'],
437 437 },
438 438 permission='pull')
439 439 def knownv2(repo, proto, nodes=None):
440 440 nodes = nodes or []
441 441 result = b''.join(b'1' if n else b'0' for n in repo.known(nodes))
442 442 return wireprototypes.cborresponse(result)
443 443
444 444 @wireprotocommand('listkeys',
445 445 args={
446 446 'namespace': b'ns',
447 447 },
448 448 permission='pull')
449 449 def listkeysv2(repo, proto, namespace=None):
450 450 keys = repo.listkeys(encoding.tolocal(namespace))
451 451 keys = {encoding.fromlocal(k): encoding.fromlocal(v)
452 452 for k, v in keys.iteritems()}
453 453
454 454 return wireprototypes.cborresponse(keys)
455 455
456 456 @wireprotocommand('lookup',
457 457 args={
458 458 'key': b'foo',
459 459 },
460 460 permission='pull')
461 461 def lookupv2(repo, proto, key):
462 462 key = encoding.tolocal(key)
463 463
464 464 # TODO handle exception.
465 465 node = repo.lookup(key)
466 466
467 467 return wireprototypes.cborresponse(node)
468 468
469 469 @wireprotocommand('pushkey',
470 470 args={
471 471 'namespace': b'ns',
472 472 'key': b'key',
473 473 'old': b'old',
474 474 'new': b'new',
475 475 },
476 476 permission='push')
477 477 def pushkeyv2(repo, proto, namespace, key, old, new):
478 478 # TODO handle ui output redirection
479 479 r = repo.pushkey(encoding.tolocal(namespace),
480 480 encoding.tolocal(key),
481 481 encoding.tolocal(old),
482 482 encoding.tolocal(new))
483 483
484 484 return wireprototypes.cborresponse(r)
@@ -1,564 +1,564 b''
1 1 $ . $TESTDIR/wireprotohelpers.sh
2 2 $ enabledummycommands
3 3
4 4 $ hg init server
5 5 $ cat > server/.hg/hgrc << EOF
6 6 > [experimental]
7 7 > web.apiserver = true
8 8 > EOF
9 9 $ hg -R server serve -p $HGPORT -d --pid-file hg.pid
10 10 $ cat hg.pid > $DAEMON_PIDS
11 11
12 12 HTTP v2 protocol not enabled by default
13 13
14 14 $ sendhttpraw << EOF
15 15 > httprequest GET api/$HTTPV2
16 16 > user-agent: test
17 17 > EOF
18 18 using raw connection to peer
19 19 s> GET /api/exp-http-v2-0001 HTTP/1.1\r\n
20 20 s> Accept-Encoding: identity\r\n
21 21 s> user-agent: test\r\n
22 22 s> host: $LOCALIP:$HGPORT\r\n (glob)
23 23 s> \r\n
24 24 s> makefile('rb', None)
25 25 s> HTTP/1.1 404 Not Found\r\n
26 26 s> Server: testing stub value\r\n
27 27 s> Date: $HTTP_DATE$\r\n
28 28 s> Content-Type: text/plain\r\n
29 29 s> Content-Length: 33\r\n
30 30 s> \r\n
31 31 s> API exp-http-v2-0001 not enabled\n
32 32
33 33 Restart server with support for HTTP v2 API
34 34
35 35 $ killdaemons.py
36 36 $ enablehttpv2 server
37 37 $ hg -R server serve -p $HGPORT -d --pid-file hg.pid
38 38 $ cat hg.pid > $DAEMON_PIDS
39 39
40 40 Request to unknown command yields 404
41 41
42 42 $ sendhttpraw << EOF
43 43 > httprequest POST api/$HTTPV2/ro/badcommand
44 44 > user-agent: test
45 45 > EOF
46 46 using raw connection to peer
47 47 s> POST /api/exp-http-v2-0001/ro/badcommand HTTP/1.1\r\n
48 48 s> Accept-Encoding: identity\r\n
49 49 s> user-agent: test\r\n
50 50 s> host: $LOCALIP:$HGPORT\r\n (glob)
51 51 s> \r\n
52 52 s> makefile('rb', None)
53 53 s> HTTP/1.1 404 Not Found\r\n
54 54 s> Server: testing stub value\r\n
55 55 s> Date: $HTTP_DATE$\r\n
56 56 s> Content-Type: text/plain\r\n
57 57 s> Content-Length: 42\r\n
58 58 s> \r\n
59 59 s> unknown wire protocol command: badcommand\n
60 60
61 61 GET to read-only command yields a 405
62 62
63 63 $ sendhttpraw << EOF
64 64 > httprequest GET api/$HTTPV2/ro/customreadonly
65 65 > user-agent: test
66 66 > EOF
67 67 using raw connection to peer
68 68 s> GET /api/exp-http-v2-0001/ro/customreadonly HTTP/1.1\r\n
69 69 s> Accept-Encoding: identity\r\n
70 70 s> user-agent: test\r\n
71 71 s> host: $LOCALIP:$HGPORT\r\n (glob)
72 72 s> \r\n
73 73 s> makefile('rb', None)
74 74 s> HTTP/1.1 405 Method Not Allowed\r\n
75 75 s> Server: testing stub value\r\n
76 76 s> Date: $HTTP_DATE$\r\n
77 77 s> Allow: POST\r\n
78 78 s> Content-Length: 30\r\n
79 79 s> \r\n
80 80 s> commands require POST requests
81 81
82 82 Missing Accept header results in 406
83 83
84 84 $ sendhttpraw << EOF
85 85 > httprequest POST api/$HTTPV2/ro/customreadonly
86 86 > user-agent: test
87 87 > EOF
88 88 using raw connection to peer
89 89 s> POST /api/exp-http-v2-0001/ro/customreadonly HTTP/1.1\r\n
90 90 s> Accept-Encoding: identity\r\n
91 91 s> user-agent: test\r\n
92 92 s> host: $LOCALIP:$HGPORT\r\n (glob)
93 93 s> \r\n
94 94 s> makefile('rb', None)
95 95 s> HTTP/1.1 406 Not Acceptable\r\n
96 96 s> Server: testing stub value\r\n
97 97 s> Date: $HTTP_DATE$\r\n
98 98 s> Content-Type: text/plain\r\n
99 99 s> Content-Length: 85\r\n
100 100 s> \r\n
101 s> client MUST specify Accept header with value: application/mercurial-exp-framing-0004\n
101 s> client MUST specify Accept header with value: application/mercurial-exp-framing-0005\n
102 102
103 103 Bad Accept header results in 406
104 104
105 105 $ sendhttpraw << EOF
106 106 > httprequest POST api/$HTTPV2/ro/customreadonly
107 107 > accept: invalid
108 108 > user-agent: test
109 109 > EOF
110 110 using raw connection to peer
111 111 s> POST /api/exp-http-v2-0001/ro/customreadonly HTTP/1.1\r\n
112 112 s> Accept-Encoding: identity\r\n
113 113 s> accept: invalid\r\n
114 114 s> user-agent: test\r\n
115 115 s> host: $LOCALIP:$HGPORT\r\n (glob)
116 116 s> \r\n
117 117 s> makefile('rb', None)
118 118 s> HTTP/1.1 406 Not Acceptable\r\n
119 119 s> Server: testing stub value\r\n
120 120 s> Date: $HTTP_DATE$\r\n
121 121 s> Content-Type: text/plain\r\n
122 122 s> Content-Length: 85\r\n
123 123 s> \r\n
124 s> client MUST specify Accept header with value: application/mercurial-exp-framing-0004\n
124 s> client MUST specify Accept header with value: application/mercurial-exp-framing-0005\n
125 125
126 126 Bad Content-Type header results in 415
127 127
128 128 $ sendhttpraw << EOF
129 129 > httprequest POST api/$HTTPV2/ro/customreadonly
130 130 > accept: $MEDIATYPE
131 131 > user-agent: test
132 132 > content-type: badmedia
133 133 > EOF
134 134 using raw connection to peer
135 135 s> POST /api/exp-http-v2-0001/ro/customreadonly HTTP/1.1\r\n
136 136 s> Accept-Encoding: identity\r\n
137 s> accept: application/mercurial-exp-framing-0004\r\n
137 s> accept: application/mercurial-exp-framing-0005\r\n
138 138 s> content-type: badmedia\r\n
139 139 s> user-agent: test\r\n
140 140 s> host: $LOCALIP:$HGPORT\r\n (glob)
141 141 s> \r\n
142 142 s> makefile('rb', None)
143 143 s> HTTP/1.1 415 Unsupported Media Type\r\n
144 144 s> Server: testing stub value\r\n
145 145 s> Date: $HTTP_DATE$\r\n
146 146 s> Content-Type: text/plain\r\n
147 147 s> Content-Length: 88\r\n
148 148 s> \r\n
149 s> client MUST send Content-Type header with value: application/mercurial-exp-framing-0004\n
149 s> client MUST send Content-Type header with value: application/mercurial-exp-framing-0005\n
150 150
151 151 Request to read-only command works out of the box
152 152
153 153 $ sendhttpraw << EOF
154 154 > httprequest POST api/$HTTPV2/ro/customreadonly
155 155 > accept: $MEDIATYPE
156 156 > content-type: $MEDIATYPE
157 157 > user-agent: test
158 158 > frame 1 1 stream-begin command-request new cbor:{b'name': b'customreadonly'}
159 159 > EOF
160 160 using raw connection to peer
161 161 s> POST /api/exp-http-v2-0001/ro/customreadonly HTTP/1.1\r\n
162 162 s> Accept-Encoding: identity\r\n
163 163 s> *\r\n (glob)
164 s> content-type: application/mercurial-exp-framing-0004\r\n
164 s> content-type: application/mercurial-exp-framing-0005\r\n
165 165 s> user-agent: test\r\n
166 166 s> content-length: 29\r\n
167 167 s> host: $LOCALIP:$HGPORT\r\n (glob)
168 168 s> \r\n
169 169 s> \x15\x00\x00\x01\x00\x01\x01\x11\xa1DnameNcustomreadonly
170 170 s> makefile('rb', None)
171 171 s> HTTP/1.1 200 OK\r\n
172 172 s> Server: testing stub value\r\n
173 173 s> Date: $HTTP_DATE$\r\n
174 s> Content-Type: application/mercurial-exp-framing-0004\r\n
174 s> Content-Type: application/mercurial-exp-framing-0005\r\n
175 175 s> Transfer-Encoding: chunked\r\n
176 176 s> \r\n
177 s> 27\r\n
178 s> \x1f\x00\x00\x01\x00\x02\x012X\x1dcustomreadonly bytes response
177 s> 32\r\n
178 s> *\x00\x00\x01\x00\x02\x012\xa1FstatusBokX\x1dcustomreadonly bytes response
179 179 s> \r\n
180 180 s> 0\r\n
181 181 s> \r\n
182 182
183 183 $ sendhttpv2peer << EOF
184 184 > command customreadonly
185 185 > EOF
186 186 creating http peer for wire protocol version 2
187 187 sending customreadonly command
188 188 s> POST /api/exp-http-v2-0001/ro/customreadonly HTTP/1.1\r\n
189 189 s> Accept-Encoding: identity\r\n
190 s> accept: application/mercurial-exp-framing-0004\r\n
191 s> content-type: application/mercurial-exp-framing-0004\r\n
190 s> accept: application/mercurial-exp-framing-0005\r\n
191 s> content-type: application/mercurial-exp-framing-0005\r\n
192 192 s> content-length: 29\r\n
193 193 s> host: $LOCALIP:$HGPORT\r\n (glob)
194 194 s> user-agent: Mercurial debugwireproto\r\n
195 195 s> \r\n
196 196 s> \x15\x00\x00\x01\x00\x01\x01\x11\xa1DnameNcustomreadonly
197 197 s> makefile('rb', None)
198 198 s> HTTP/1.1 200 OK\r\n
199 199 s> Server: testing stub value\r\n
200 200 s> Date: $HTTP_DATE$\r\n
201 s> Content-Type: application/mercurial-exp-framing-0004\r\n
201 s> Content-Type: application/mercurial-exp-framing-0005\r\n
202 202 s> Transfer-Encoding: chunked\r\n
203 203 s> \r\n
204 s> 27\r\n
205 s> \x1f\x00\x00\x01\x00\x02\x012
206 s> X\x1dcustomreadonly bytes response
204 s> 32\r\n
205 s> *\x00\x00\x01\x00\x02\x012
206 s> \xa1FstatusBokX\x1dcustomreadonly bytes response
207 207 s> \r\n
208 received frame(size=31; request=1; stream=2; streamflags=stream-begin; type=command-response; flags=eos)
208 received frame(size=42; request=1; stream=2; streamflags=stream-begin; type=command-response; flags=eos)
209 209 s> 0\r\n
210 210 s> \r\n
211 response: [b'customreadonly bytes response']
211 response: [{b'status': b'ok'}, b'customreadonly bytes response']
212 212
213 213 Request to read-write command fails because server is read-only by default
214 214
215 215 GET to read-write request yields 405
216 216
217 217 $ sendhttpraw << EOF
218 218 > httprequest GET api/$HTTPV2/rw/customreadonly
219 219 > user-agent: test
220 220 > EOF
221 221 using raw connection to peer
222 222 s> GET /api/exp-http-v2-0001/rw/customreadonly HTTP/1.1\r\n
223 223 s> Accept-Encoding: identity\r\n
224 224 s> user-agent: test\r\n
225 225 s> host: $LOCALIP:$HGPORT\r\n (glob)
226 226 s> \r\n
227 227 s> makefile('rb', None)
228 228 s> HTTP/1.1 405 Method Not Allowed\r\n
229 229 s> Server: testing stub value\r\n
230 230 s> Date: $HTTP_DATE$\r\n
231 231 s> Allow: POST\r\n
232 232 s> Content-Length: 30\r\n
233 233 s> \r\n
234 234 s> commands require POST requests
235 235
236 236 Even for unknown commands
237 237
238 238 $ sendhttpraw << EOF
239 239 > httprequest GET api/$HTTPV2/rw/badcommand
240 240 > user-agent: test
241 241 > EOF
242 242 using raw connection to peer
243 243 s> GET /api/exp-http-v2-0001/rw/badcommand HTTP/1.1\r\n
244 244 s> Accept-Encoding: identity\r\n
245 245 s> user-agent: test\r\n
246 246 s> host: $LOCALIP:$HGPORT\r\n (glob)
247 247 s> \r\n
248 248 s> makefile('rb', None)
249 249 s> HTTP/1.1 405 Method Not Allowed\r\n
250 250 s> Server: testing stub value\r\n
251 251 s> Date: $HTTP_DATE$\r\n
252 252 s> Allow: POST\r\n
253 253 s> Content-Length: 30\r\n
254 254 s> \r\n
255 255 s> commands require POST requests
256 256
257 257 SSL required by default
258 258
259 259 $ sendhttpraw << EOF
260 260 > httprequest POST api/$HTTPV2/rw/customreadonly
261 261 > user-agent: test
262 262 > EOF
263 263 using raw connection to peer
264 264 s> POST /api/exp-http-v2-0001/rw/customreadonly HTTP/1.1\r\n
265 265 s> Accept-Encoding: identity\r\n
266 266 s> user-agent: test\r\n
267 267 s> host: $LOCALIP:$HGPORT\r\n (glob)
268 268 s> \r\n
269 269 s> makefile('rb', None)
270 270 s> HTTP/1.1 403 ssl required\r\n
271 271 s> Server: testing stub value\r\n
272 272 s> Date: $HTTP_DATE$\r\n
273 273 s> Content-Length: 17\r\n
274 274 s> \r\n
275 275 s> permission denied
276 276
277 277 Restart server to allow non-ssl read-write operations
278 278
279 279 $ killdaemons.py
280 280 $ cat > server/.hg/hgrc << EOF
281 281 > [experimental]
282 282 > web.apiserver = true
283 283 > web.api.http-v2 = true
284 284 > [web]
285 285 > push_ssl = false
286 286 > allow-push = *
287 287 > EOF
288 288
289 289 $ hg -R server serve -p $HGPORT -d --pid-file hg.pid -E error.log
290 290 $ cat hg.pid > $DAEMON_PIDS
291 291
292 292 Authorized request for valid read-write command works
293 293
294 294 $ sendhttpraw << EOF
295 295 > httprequest POST api/$HTTPV2/rw/customreadonly
296 296 > user-agent: test
297 297 > accept: $MEDIATYPE
298 298 > content-type: $MEDIATYPE
299 299 > frame 1 1 stream-begin command-request new cbor:{b'name': b'customreadonly'}
300 300 > EOF
301 301 using raw connection to peer
302 302 s> POST /api/exp-http-v2-0001/rw/customreadonly HTTP/1.1\r\n
303 303 s> Accept-Encoding: identity\r\n
304 s> accept: application/mercurial-exp-framing-0004\r\n
305 s> content-type: application/mercurial-exp-framing-0004\r\n
304 s> accept: application/mercurial-exp-framing-0005\r\n
305 s> content-type: application/mercurial-exp-framing-0005\r\n
306 306 s> user-agent: test\r\n
307 307 s> content-length: 29\r\n
308 308 s> host: $LOCALIP:$HGPORT\r\n (glob)
309 309 s> \r\n
310 310 s> \x15\x00\x00\x01\x00\x01\x01\x11\xa1DnameNcustomreadonly
311 311 s> makefile('rb', None)
312 312 s> HTTP/1.1 200 OK\r\n
313 313 s> Server: testing stub value\r\n
314 314 s> Date: $HTTP_DATE$\r\n
315 s> Content-Type: application/mercurial-exp-framing-0004\r\n
315 s> Content-Type: application/mercurial-exp-framing-0005\r\n
316 316 s> Transfer-Encoding: chunked\r\n
317 317 s> \r\n
318 s> 27\r\n
319 s> \x1f\x00\x00\x01\x00\x02\x012X\x1dcustomreadonly bytes response
318 s> 32\r\n
319 s> *\x00\x00\x01\x00\x02\x012\xa1FstatusBokX\x1dcustomreadonly bytes response
320 320 s> \r\n
321 321 s> 0\r\n
322 322 s> \r\n
323 323
324 324 Authorized request for unknown command is rejected
325 325
326 326 $ sendhttpraw << EOF
327 327 > httprequest POST api/$HTTPV2/rw/badcommand
328 328 > user-agent: test
329 329 > accept: $MEDIATYPE
330 330 > EOF
331 331 using raw connection to peer
332 332 s> POST /api/exp-http-v2-0001/rw/badcommand HTTP/1.1\r\n
333 333 s> Accept-Encoding: identity\r\n
334 s> accept: application/mercurial-exp-framing-0004\r\n
334 s> accept: application/mercurial-exp-framing-0005\r\n
335 335 s> user-agent: test\r\n
336 336 s> host: $LOCALIP:$HGPORT\r\n (glob)
337 337 s> \r\n
338 338 s> makefile('rb', None)
339 339 s> HTTP/1.1 404 Not Found\r\n
340 340 s> Server: testing stub value\r\n
341 341 s> Date: $HTTP_DATE$\r\n
342 342 s> Content-Type: text/plain\r\n
343 343 s> Content-Length: 42\r\n
344 344 s> \r\n
345 345 s> unknown wire protocol command: badcommand\n
346 346
347 347 debugreflect isn't enabled by default
348 348
349 349 $ sendhttpraw << EOF
350 350 > httprequest POST api/$HTTPV2/ro/debugreflect
351 351 > user-agent: test
352 352 > EOF
353 353 using raw connection to peer
354 354 s> POST /api/exp-http-v2-0001/ro/debugreflect HTTP/1.1\r\n
355 355 s> Accept-Encoding: identity\r\n
356 356 s> user-agent: test\r\n
357 357 s> host: $LOCALIP:$HGPORT\r\n (glob)
358 358 s> \r\n
359 359 s> makefile('rb', None)
360 360 s> HTTP/1.1 404 Not Found\r\n
361 361 s> Server: testing stub value\r\n
362 362 s> Date: $HTTP_DATE$\r\n
363 363 s> Content-Type: text/plain\r\n
364 364 s> Content-Length: 34\r\n
365 365 s> \r\n
366 366 s> debugreflect service not available
367 367
368 368 Restart server to get debugreflect endpoint
369 369
370 370 $ killdaemons.py
371 371 $ cat > server/.hg/hgrc << EOF
372 372 > [experimental]
373 373 > web.apiserver = true
374 374 > web.api.debugreflect = true
375 375 > web.api.http-v2 = true
376 376 > [web]
377 377 > push_ssl = false
378 378 > allow-push = *
379 379 > EOF
380 380
381 381 $ hg -R server serve -p $HGPORT -d --pid-file hg.pid -E error.log
382 382 $ cat hg.pid > $DAEMON_PIDS
383 383
384 384 Command frames can be reflected via debugreflect
385 385
386 386 $ sendhttpraw << EOF
387 387 > httprequest POST api/$HTTPV2/ro/debugreflect
388 388 > accept: $MEDIATYPE
389 389 > content-type: $MEDIATYPE
390 390 > user-agent: test
391 391 > frame 1 1 stream-begin command-request new cbor:{b'name': b'command1', b'args': {b'foo': b'val1', b'bar1': b'val'}}
392 392 > EOF
393 393 using raw connection to peer
394 394 s> POST /api/exp-http-v2-0001/ro/debugreflect HTTP/1.1\r\n
395 395 s> Accept-Encoding: identity\r\n
396 s> accept: application/mercurial-exp-framing-0004\r\n
397 s> content-type: application/mercurial-exp-framing-0004\r\n
396 s> accept: application/mercurial-exp-framing-0005\r\n
397 s> content-type: application/mercurial-exp-framing-0005\r\n
398 398 s> user-agent: test\r\n
399 399 s> content-length: 47\r\n
400 400 s> host: $LOCALIP:$HGPORT\r\n (glob)
401 401 s> \r\n
402 402 s> \'\x00\x00\x01\x00\x01\x01\x11\xa2Dargs\xa2CfooDval1Dbar1CvalDnameHcommand1
403 403 s> makefile('rb', None)
404 404 s> HTTP/1.1 200 OK\r\n
405 405 s> Server: testing stub value\r\n
406 406 s> Date: $HTTP_DATE$\r\n
407 407 s> Content-Type: text/plain\r\n
408 408 s> Content-Length: 205\r\n
409 409 s> \r\n
410 410 s> received: 1 1 1 \xa2Dargs\xa2CfooDval1Dbar1CvalDnameHcommand1\n
411 411 s> ["runcommand", {"args": {"bar1": "val", "foo": "val1"}, "command": "command1", "data": null, "requestid": 1}]\n
412 412 s> received: <no frame>\n
413 413 s> {"action": "noop"}
414 414
415 415 Multiple requests to regular command URL are not allowed
416 416
417 417 $ sendhttpraw << EOF
418 418 > httprequest POST api/$HTTPV2/ro/customreadonly
419 419 > accept: $MEDIATYPE
420 420 > content-type: $MEDIATYPE
421 421 > user-agent: test
422 422 > frame 1 1 stream-begin command-request new cbor:{b'name': b'customreadonly'}
423 423 > EOF
424 424 using raw connection to peer
425 425 s> POST /api/exp-http-v2-0001/ro/customreadonly HTTP/1.1\r\n
426 426 s> Accept-Encoding: identity\r\n
427 s> accept: application/mercurial-exp-framing-0004\r\n
428 s> content-type: application/mercurial-exp-framing-0004\r\n
427 s> accept: application/mercurial-exp-framing-0005\r\n
428 s> content-type: application/mercurial-exp-framing-0005\r\n
429 429 s> user-agent: test\r\n
430 430 s> content-length: 29\r\n
431 431 s> host: $LOCALIP:$HGPORT\r\n (glob)
432 432 s> \r\n
433 433 s> \x15\x00\x00\x01\x00\x01\x01\x11\xa1DnameNcustomreadonly
434 434 s> makefile('rb', None)
435 435 s> HTTP/1.1 200 OK\r\n
436 436 s> Server: testing stub value\r\n
437 437 s> Date: $HTTP_DATE$\r\n
438 s> Content-Type: application/mercurial-exp-framing-0004\r\n
438 s> Content-Type: application/mercurial-exp-framing-0005\r\n
439 439 s> Transfer-Encoding: chunked\r\n
440 440 s> \r\n
441 s> 27\r\n
442 s> \x1f\x00\x00\x01\x00\x02\x012X\x1dcustomreadonly bytes response
441 s> 32\r\n
442 s> *\x00\x00\x01\x00\x02\x012\xa1FstatusBokX\x1dcustomreadonly bytes response
443 443 s> \r\n
444 444 s> 0\r\n
445 445 s> \r\n
446 446
447 447 Multiple requests to "multirequest" URL are allowed
448 448
449 449 $ sendhttpraw << EOF
450 450 > httprequest POST api/$HTTPV2/ro/multirequest
451 451 > accept: $MEDIATYPE
452 452 > content-type: $MEDIATYPE
453 453 > user-agent: test
454 454 > frame 1 1 stream-begin command-request new cbor:{b'name': b'customreadonly'}
455 455 > frame 3 1 0 command-request new cbor:{b'name': b'customreadonly'}
456 456 > EOF
457 457 using raw connection to peer
458 458 s> POST /api/exp-http-v2-0001/ro/multirequest HTTP/1.1\r\n
459 459 s> Accept-Encoding: identity\r\n
460 460 s> *\r\n (glob)
461 461 s> *\r\n (glob)
462 462 s> user-agent: test\r\n
463 463 s> content-length: 58\r\n
464 464 s> host: $LOCALIP:$HGPORT\r\n (glob)
465 465 s> \r\n
466 466 s> \x15\x00\x00\x01\x00\x01\x01\x11\xa1DnameNcustomreadonly\x15\x00\x00\x03\x00\x01\x00\x11\xa1DnameNcustomreadonly
467 467 s> makefile('rb', None)
468 468 s> HTTP/1.1 200 OK\r\n
469 469 s> Server: testing stub value\r\n
470 470 s> Date: $HTTP_DATE$\r\n
471 s> Content-Type: application/mercurial-exp-framing-0004\r\n
471 s> Content-Type: application/mercurial-exp-framing-0005\r\n
472 472 s> Transfer-Encoding: chunked\r\n
473 473 s> \r\n
474 s> 27\r\n
475 s> \x1f\x00\x00\x01\x00\x02\x012X\x1dcustomreadonly bytes response
474 s> 32\r\n
475 s> *\x00\x00\x01\x00\x02\x012\xa1FstatusBokX\x1dcustomreadonly bytes response
476 476 s> \r\n
477 s> 27\r\n
478 s> \x1f\x00\x00\x03\x00\x02\x002X\x1dcustomreadonly bytes response
477 s> 32\r\n
478 s> *\x00\x00\x03\x00\x02\x002\xa1FstatusBokX\x1dcustomreadonly bytes response
479 479 s> \r\n
480 480 s> 0\r\n
481 481 s> \r\n
482 482
483 483 Interleaved requests to "multirequest" are processed
484 484
485 485 $ sendhttpraw << EOF
486 486 > httprequest POST api/$HTTPV2/ro/multirequest
487 487 > accept: $MEDIATYPE
488 488 > content-type: $MEDIATYPE
489 489 > user-agent: test
490 490 > frame 1 1 stream-begin command-request new|more \xa2Dargs\xa1Inamespace
491 491 > frame 3 1 0 command-request new|more \xa2Dargs\xa1Inamespace
492 492 > frame 3 1 0 command-request continuation JnamespacesDnameHlistkeys
493 493 > frame 1 1 0 command-request continuation IbookmarksDnameHlistkeys
494 494 > EOF
495 495 using raw connection to peer
496 496 s> POST /api/exp-http-v2-0001/ro/multirequest HTTP/1.1\r\n
497 497 s> Accept-Encoding: identity\r\n
498 s> accept: application/mercurial-exp-framing-0004\r\n
499 s> content-type: application/mercurial-exp-framing-0004\r\n
498 s> accept: application/mercurial-exp-framing-0005\r\n
499 s> content-type: application/mercurial-exp-framing-0005\r\n
500 500 s> user-agent: test\r\n
501 501 s> content-length: 115\r\n
502 502 s> host: $LOCALIP:$HGPORT\r\n (glob)
503 503 s> \r\n
504 504 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
505 505 s> makefile('rb', None)
506 506 s> HTTP/1.1 200 OK\r\n
507 507 s> Server: testing stub value\r\n
508 508 s> Date: $HTTP_DATE$\r\n
509 s> Content-Type: application/mercurial-exp-framing-0004\r\n
509 s> Content-Type: application/mercurial-exp-framing-0005\r\n
510 510 s> Transfer-Encoding: chunked\r\n
511 511 s> \r\n
512 s> 28\r\n
513 s> \x00\x00\x03\x00\x02\x012\xa3Fphases@Ibookmarks@Jnamespaces@
512 s> 33\r\n
513 s> +\x00\x00\x03\x00\x02\x012\xa1FstatusBok\xa3Fphases@Ibookmarks@Jnamespaces@
514 514 s> \r\n
515 s> 9\r\n
516 s> \x01\x00\x00\x01\x00\x02\x002\xa0
515 s> 14\r\n
516 s> \x0c\x00\x00\x01\x00\x02\x002\xa1FstatusBok\xa0
517 517 s> \r\n
518 518 s> 0\r\n
519 519 s> \r\n
520 520
521 521 Restart server to disable read-write access
522 522
523 523 $ killdaemons.py
524 524 $ cat > server/.hg/hgrc << EOF
525 525 > [experimental]
526 526 > web.apiserver = true
527 527 > web.api.debugreflect = true
528 528 > web.api.http-v2 = true
529 529 > [web]
530 530 > push_ssl = false
531 531 > EOF
532 532
533 533 $ hg -R server serve -p $HGPORT -d --pid-file hg.pid -E error.log
534 534 $ cat hg.pid > $DAEMON_PIDS
535 535
536 536 Attempting to run a read-write command via multirequest on read-only URL is not allowed
537 537
538 538 $ sendhttpraw << EOF
539 539 > httprequest POST api/$HTTPV2/ro/multirequest
540 540 > accept: $MEDIATYPE
541 541 > content-type: $MEDIATYPE
542 542 > user-agent: test
543 543 > frame 1 1 stream-begin command-request new cbor:{b'name': b'pushkey'}
544 544 > EOF
545 545 using raw connection to peer
546 546 s> POST /api/exp-http-v2-0001/ro/multirequest HTTP/1.1\r\n
547 547 s> Accept-Encoding: identity\r\n
548 s> accept: application/mercurial-exp-framing-0004\r\n
549 s> content-type: application/mercurial-exp-framing-0004\r\n
548 s> accept: application/mercurial-exp-framing-0005\r\n
549 s> content-type: application/mercurial-exp-framing-0005\r\n
550 550 s> user-agent: test\r\n
551 551 s> content-length: 22\r\n
552 552 s> host: $LOCALIP:$HGPORT\r\n (glob)
553 553 s> \r\n
554 554 s> \x0e\x00\x00\x01\x00\x01\x01\x11\xa1DnameGpushkey
555 555 s> makefile('rb', None)
556 556 s> HTTP/1.1 403 Forbidden\r\n
557 557 s> Server: testing stub value\r\n
558 558 s> Date: $HTTP_DATE$\r\n
559 559 s> Content-Type: text/plain\r\n
560 560 s> Content-Length: 52\r\n
561 561 s> \r\n
562 562 s> insufficient permissions to execute command: pushkey
563 563
564 564 $ cat error.log
@@ -1,335 +1,335 b''
1 1 $ . $TESTDIR/wireprotohelpers.sh
2 2
3 3 $ cat >> $HGRCPATH << EOF
4 4 > [web]
5 5 > push_ssl = false
6 6 > allow_push = *
7 7 > EOF
8 8
9 9 $ hg init server
10 10 $ cd server
11 11 $ touch a
12 12 $ hg -q commit -A -m initial
13 13 $ cd ..
14 14
15 15 $ hg serve -R server -p $HGPORT -d --pid-file hg.pid
16 16 $ cat hg.pid >> $DAEMON_PIDS
17 17
18 18 compression formats are advertised in compression capability
19 19
20 20 #if zstd
21 21 $ get-with-headers.py $LOCALIP:$HGPORT '?cmd=capabilities' | tr ' ' '\n' | grep '^compression=zstd,zlib$' > /dev/null
22 22 #else
23 23 $ get-with-headers.py $LOCALIP:$HGPORT '?cmd=capabilities' | tr ' ' '\n' | grep '^compression=zlib$' > /dev/null
24 24 #endif
25 25
26 26 $ killdaemons.py
27 27
28 28 server.compressionengines can replace engines list wholesale
29 29
30 30 $ hg serve --config server.compressionengines=none -R server -p $HGPORT -d --pid-file hg.pid
31 31 $ cat hg.pid > $DAEMON_PIDS
32 32 $ get-with-headers.py $LOCALIP:$HGPORT '?cmd=capabilities' | tr ' ' '\n' | grep '^compression=none$' > /dev/null
33 33
34 34 $ killdaemons.py
35 35
36 36 Order of engines can also change
37 37
38 38 $ hg serve --config server.compressionengines=none,zlib -R server -p $HGPORT -d --pid-file hg.pid
39 39 $ cat hg.pid > $DAEMON_PIDS
40 40 $ get-with-headers.py $LOCALIP:$HGPORT '?cmd=capabilities' | tr ' ' '\n' | grep '^compression=none,zlib$' > /dev/null
41 41
42 42 $ killdaemons.py
43 43
44 44 Start a default server again
45 45
46 46 $ hg serve -R server -p $HGPORT -d --pid-file hg.pid
47 47 $ cat hg.pid > $DAEMON_PIDS
48 48
49 49 Server should send application/mercurial-0.1 to clients if no Accept is used
50 50
51 51 $ get-with-headers.py --headeronly $LOCALIP:$HGPORT '?cmd=getbundle&heads=e93700bd72895c5addab234c56d4024b487a362f&common=0000000000000000000000000000000000000000' -
52 52 200 Script output follows
53 53 content-type: application/mercurial-0.1
54 54 date: $HTTP_DATE$
55 55 server: testing stub value
56 56 transfer-encoding: chunked
57 57
58 58 Server should send application/mercurial-0.1 when client says it wants it
59 59
60 60 $ get-with-headers.py --hgproto '0.1' --headeronly $LOCALIP:$HGPORT '?cmd=getbundle&heads=e93700bd72895c5addab234c56d4024b487a362f&common=0000000000000000000000000000000000000000' -
61 61 200 Script output follows
62 62 content-type: application/mercurial-0.1
63 63 date: $HTTP_DATE$
64 64 server: testing stub value
65 65 transfer-encoding: chunked
66 66
67 67 Server should send application/mercurial-0.2 when client says it wants it
68 68
69 69 $ get-with-headers.py --hgproto '0.2' --headeronly $LOCALIP:$HGPORT '?cmd=getbundle&heads=e93700bd72895c5addab234c56d4024b487a362f&common=0000000000000000000000000000000000000000' -
70 70 200 Script output follows
71 71 content-type: application/mercurial-0.2
72 72 date: $HTTP_DATE$
73 73 server: testing stub value
74 74 transfer-encoding: chunked
75 75
76 76 $ get-with-headers.py --hgproto '0.1 0.2' --headeronly $LOCALIP:$HGPORT '?cmd=getbundle&heads=e93700bd72895c5addab234c56d4024b487a362f&common=0000000000000000000000000000000000000000' -
77 77 200 Script output follows
78 78 content-type: application/mercurial-0.2
79 79 date: $HTTP_DATE$
80 80 server: testing stub value
81 81 transfer-encoding: chunked
82 82
83 83 Requesting a compression format that server doesn't support results will fall back to 0.1
84 84
85 85 $ get-with-headers.py --hgproto '0.2 comp=aa' --headeronly $LOCALIP:$HGPORT '?cmd=getbundle&heads=e93700bd72895c5addab234c56d4024b487a362f&common=0000000000000000000000000000000000000000' -
86 86 200 Script output follows
87 87 content-type: application/mercurial-0.1
88 88 date: $HTTP_DATE$
89 89 server: testing stub value
90 90 transfer-encoding: chunked
91 91
92 92 #if zstd
93 93 zstd is used if available
94 94
95 95 $ get-with-headers.py --hgproto '0.2 comp=zstd' $LOCALIP:$HGPORT '?cmd=getbundle&heads=e93700bd72895c5addab234c56d4024b487a362f&common=0000000000000000000000000000000000000000' > resp
96 96 $ f --size --hexdump --bytes 36 --sha1 resp
97 97 resp: size=248, sha1=4d8d8f87fb82bd542ce52881fdc94f850748
98 98 0000: 32 30 30 20 53 63 72 69 70 74 20 6f 75 74 70 75 |200 Script outpu|
99 99 0010: 74 20 66 6f 6c 6c 6f 77 73 0a 0a 04 7a 73 74 64 |t follows...zstd|
100 100 0020: 28 b5 2f fd |(./.|
101 101
102 102 #endif
103 103
104 104 application/mercurial-0.2 is not yet used on non-streaming responses
105 105
106 106 $ get-with-headers.py --hgproto '0.2' $LOCALIP:$HGPORT '?cmd=heads' -
107 107 200 Script output follows
108 108 content-length: 41
109 109 content-type: application/mercurial-0.1
110 110 date: $HTTP_DATE$
111 111 server: testing stub value
112 112
113 113 e93700bd72895c5addab234c56d4024b487a362f
114 114
115 115 Now test protocol preference usage
116 116
117 117 $ killdaemons.py
118 118 $ hg serve --config server.compressionengines=none,zlib -R server -p $HGPORT -d --pid-file hg.pid
119 119 $ cat hg.pid > $DAEMON_PIDS
120 120
121 121 No Accept will send 0.1+zlib, even though "none" is preferred b/c "none" isn't supported on 0.1
122 122
123 123 $ get-with-headers.py --headeronly $LOCALIP:$HGPORT '?cmd=getbundle&heads=e93700bd72895c5addab234c56d4024b487a362f&common=0000000000000000000000000000000000000000' Content-Type
124 124 200 Script output follows
125 125 content-type: application/mercurial-0.1
126 126
127 127 $ get-with-headers.py $LOCALIP:$HGPORT '?cmd=getbundle&heads=e93700bd72895c5addab234c56d4024b487a362f&common=0000000000000000000000000000000000000000' > resp
128 128 $ f --size --hexdump --bytes 28 --sha1 resp
129 129 resp: size=227, sha1=35a4c074da74f32f5440da3cbf04
130 130 0000: 32 30 30 20 53 63 72 69 70 74 20 6f 75 74 70 75 |200 Script outpu|
131 131 0010: 74 20 66 6f 6c 6c 6f 77 73 0a 0a 78 |t follows..x|
132 132
133 133 Explicit 0.1 will send zlib because "none" isn't supported on 0.1
134 134
135 135 $ get-with-headers.py --hgproto '0.1' $LOCALIP:$HGPORT '?cmd=getbundle&heads=e93700bd72895c5addab234c56d4024b487a362f&common=0000000000000000000000000000000000000000' > resp
136 136 $ f --size --hexdump --bytes 28 --sha1 resp
137 137 resp: size=227, sha1=35a4c074da74f32f5440da3cbf04
138 138 0000: 32 30 30 20 53 63 72 69 70 74 20 6f 75 74 70 75 |200 Script outpu|
139 139 0010: 74 20 66 6f 6c 6c 6f 77 73 0a 0a 78 |t follows..x|
140 140
141 141 0.2 with no compression will get "none" because that is server's preference
142 142 (spec says ZL and UN are implicitly supported)
143 143
144 144 $ get-with-headers.py --hgproto '0.2' $LOCALIP:$HGPORT '?cmd=getbundle&heads=e93700bd72895c5addab234c56d4024b487a362f&common=0000000000000000000000000000000000000000' > resp
145 145 $ f --size --hexdump --bytes 32 --sha1 resp
146 146 resp: size=432, sha1=ac931b412ec185a02e0e5bcff98dac83
147 147 0000: 32 30 30 20 53 63 72 69 70 74 20 6f 75 74 70 75 |200 Script outpu|
148 148 0010: 74 20 66 6f 6c 6c 6f 77 73 0a 0a 04 6e 6f 6e 65 |t follows...none|
149 149
150 150 Client receives server preference even if local order doesn't match
151 151
152 152 $ get-with-headers.py --hgproto '0.2 comp=zlib,none' $LOCALIP:$HGPORT '?cmd=getbundle&heads=e93700bd72895c5addab234c56d4024b487a362f&common=0000000000000000000000000000000000000000' > resp
153 153 $ f --size --hexdump --bytes 32 --sha1 resp
154 154 resp: size=432, sha1=ac931b412ec185a02e0e5bcff98dac83
155 155 0000: 32 30 30 20 53 63 72 69 70 74 20 6f 75 74 70 75 |200 Script outpu|
156 156 0010: 74 20 66 6f 6c 6c 6f 77 73 0a 0a 04 6e 6f 6e 65 |t follows...none|
157 157
158 158 Client receives only supported format even if not server preferred format
159 159
160 160 $ get-with-headers.py --hgproto '0.2 comp=zlib' $LOCALIP:$HGPORT '?cmd=getbundle&heads=e93700bd72895c5addab234c56d4024b487a362f&common=0000000000000000000000000000000000000000' > resp
161 161 $ f --size --hexdump --bytes 33 --sha1 resp
162 162 resp: size=232, sha1=a1c727f0c9693ca15742a75c30419bc36
163 163 0000: 32 30 30 20 53 63 72 69 70 74 20 6f 75 74 70 75 |200 Script outpu|
164 164 0010: 74 20 66 6f 6c 6c 6f 77 73 0a 0a 04 7a 6c 69 62 |t follows...zlib|
165 165 0020: 78 |x|
166 166
167 167 $ killdaemons.py
168 168 $ cd ..
169 169
170 170 Test listkeys for listing namespaces
171 171
172 172 $ hg init empty
173 173 $ hg -R empty serve -p $HGPORT -d --pid-file hg.pid
174 174 $ cat hg.pid > $DAEMON_PIDS
175 175
176 176 $ hg --verbose debugwireproto http://$LOCALIP:$HGPORT << EOF
177 177 > command listkeys
178 178 > namespace namespaces
179 179 > EOF
180 180 s> GET /?cmd=capabilities HTTP/1.1\r\n
181 181 s> Accept-Encoding: identity\r\n
182 182 s> accept: application/mercurial-0.1\r\n
183 183 s> host: $LOCALIP:$HGPORT\r\n (glob)
184 184 s> user-agent: Mercurial debugwireproto\r\n
185 185 s> \r\n
186 186 s> makefile('rb', None)
187 187 s> HTTP/1.1 200 Script output follows\r\n
188 188 s> Server: testing stub value\r\n
189 189 s> Date: $HTTP_DATE$\r\n
190 190 s> Content-Type: application/mercurial-0.1\r\n
191 191 s> Content-Length: *\r\n (glob)
192 192 s> \r\n
193 193 s> batch branchmap $USUAL_BUNDLE2_CAPS_SERVER$ 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
194 194 sending listkeys command
195 195 s> GET /?cmd=listkeys HTTP/1.1\r\n
196 196 s> Accept-Encoding: identity\r\n
197 197 s> vary: X-HgArg-1,X-HgProto-1\r\n
198 198 s> x-hgarg-1: namespace=namespaces\r\n
199 199 s> x-hgproto-1: 0.1 0.2 comp=$USUAL_COMPRESSIONS$ partial-pull\r\n
200 200 s> accept: application/mercurial-0.1\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> makefile('rb', None)
205 205 s> HTTP/1.1 200 Script output follows\r\n
206 206 s> Server: testing stub value\r\n
207 207 s> Date: $HTTP_DATE$\r\n
208 208 s> Content-Type: application/mercurial-0.1\r\n
209 209 s> Content-Length: 30\r\n
210 210 s> \r\n
211 211 s> bookmarks\t\n
212 212 s> namespaces\t\n
213 213 s> phases\t
214 214 response: {b'bookmarks': b'', b'namespaces': b'', b'phases': b''}
215 215
216 216 Same thing, but with "httprequest" command
217 217
218 218 $ hg --verbose debugwireproto --peer raw http://$LOCALIP:$HGPORT << EOF
219 219 > httprequest GET ?cmd=listkeys
220 220 > user-agent: test
221 221 > x-hgarg-1: namespace=namespaces
222 222 > EOF
223 223 using raw connection to peer
224 224 s> GET /?cmd=listkeys HTTP/1.1\r\n
225 225 s> Accept-Encoding: identity\r\n
226 226 s> user-agent: test\r\n
227 227 s> x-hgarg-1: namespace=namespaces\r\n
228 228 s> host: $LOCALIP:$HGPORT\r\n (glob)
229 229 s> \r\n
230 230 s> makefile('rb', None)
231 231 s> HTTP/1.1 200 Script output follows\r\n
232 232 s> Server: testing stub value\r\n
233 233 s> Date: $HTTP_DATE$\r\n
234 234 s> Content-Type: application/mercurial-0.1\r\n
235 235 s> Content-Length: 30\r\n
236 236 s> \r\n
237 237 s> bookmarks\t\n
238 238 s> namespaces\t\n
239 239 s> phases\t
240 240
241 241 Client with HTTPv2 enabled advertises that and gets old capabilities response from old server
242 242
243 243 $ hg --config experimental.httppeer.advertise-v2=true --verbose debugwireproto http://$LOCALIP:$HGPORT << EOF
244 244 > command heads
245 245 > EOF
246 246 s> GET /?cmd=capabilities HTTP/1.1\r\n
247 247 s> Accept-Encoding: identity\r\n
248 248 s> vary: X-HgProto-1,X-HgUpgrade-1\r\n
249 249 s> x-hgproto-1: cbor\r\n
250 250 s> x-hgupgrade-1: exp-http-v2-0001\r\n
251 251 s> accept: application/mercurial-0.1\r\n
252 252 s> host: $LOCALIP:$HGPORT\r\n (glob)
253 253 s> user-agent: Mercurial debugwireproto\r\n
254 254 s> \r\n
255 255 s> makefile('rb', None)
256 256 s> HTTP/1.1 200 Script output follows\r\n
257 257 s> Server: testing stub value\r\n
258 258 s> Date: $HTTP_DATE$\r\n
259 259 s> Content-Type: application/mercurial-0.1\r\n
260 260 s> Content-Length: 458\r\n
261 261 s> \r\n
262 262 s> batch branchmap $USUAL_BUNDLE2_CAPS_SERVER$ 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
263 263 sending heads command
264 264 s> GET /?cmd=heads HTTP/1.1\r\n
265 265 s> Accept-Encoding: identity\r\n
266 266 s> vary: X-HgProto-1\r\n
267 267 s> x-hgproto-1: 0.1 0.2 comp=$USUAL_COMPRESSIONS$ partial-pull\r\n
268 268 s> accept: application/mercurial-0.1\r\n
269 269 s> host: $LOCALIP:$HGPORT\r\n (glob)
270 270 s> user-agent: Mercurial debugwireproto\r\n
271 271 s> \r\n
272 272 s> makefile('rb', None)
273 273 s> HTTP/1.1 200 Script output follows\r\n
274 274 s> Server: testing stub value\r\n
275 275 s> Date: $HTTP_DATE$\r\n
276 276 s> Content-Type: application/mercurial-0.1\r\n
277 277 s> Content-Length: 41\r\n
278 278 s> \r\n
279 279 s> 0000000000000000000000000000000000000000\n
280 280 response: [b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00']
281 281
282 282 $ killdaemons.py
283 283 $ enablehttpv2 empty
284 284 $ hg -R empty serve -p $HGPORT -d --pid-file hg.pid
285 285 $ cat hg.pid > $DAEMON_PIDS
286 286
287 287 Client with HTTPv2 enabled automatically upgrades if the server supports it
288 288
289 289 $ hg --config experimental.httppeer.advertise-v2=true --verbose debugwireproto http://$LOCALIP:$HGPORT << EOF
290 290 > command heads
291 291 > EOF
292 292 s> GET /?cmd=capabilities HTTP/1.1\r\n
293 293 s> Accept-Encoding: identity\r\n
294 294 s> vary: X-HgProto-1,X-HgUpgrade-1\r\n
295 295 s> x-hgproto-1: cbor\r\n
296 296 s> x-hgupgrade-1: exp-http-v2-0001\r\n
297 297 s> accept: application/mercurial-0.1\r\n
298 298 s> host: $LOCALIP:$HGPORT\r\n (glob)
299 299 s> user-agent: Mercurial debugwireproto\r\n
300 300 s> \r\n
301 301 s> makefile('rb', None)
302 302 s> HTTP/1.1 200 OK\r\n
303 303 s> Server: testing stub value\r\n
304 304 s> Date: $HTTP_DATE$\r\n
305 305 s> Content-Type: application/mercurial-cbor\r\n
306 306 s> Content-Length: *\r\n (glob)
307 307 s> \r\n
308 s> \xa3Dapis\xa1Pexp-http-v2-0001\xa4Hcommands\xa7Eheads\xa2Dargs\xa1Jpubliconly\xf4Kpermissions\x81DpullEknown\xa2Dargs\xa1Enodes\x81HdeadbeefKpermissions\x81DpullFlookup\xa2Dargs\xa1CkeyCfooKpermissions\x81DpullGpushkey\xa2Dargs\xa4CkeyCkeyCnewCnewColdColdInamespaceBnsKpermissions\x81DpushHlistkeys\xa2Dargs\xa1InamespaceBnsKpermissions\x81DpullIbranchmap\xa2Dargs\xa0Kpermissions\x81DpullLcapabilities\xa2Dargs\xa0Kpermissions\x81DpullKcompression\x82\xa1DnameDzstd\xa1DnameDzlibNrawrepoformats\x82LgeneraldeltaHrevlogv1Qframingmediatypes\x81X&application/mercurial-exp-framing-0004GapibaseDapi/Nv1capabilitiesY\x01\xcabatch branchmap $USUAL_BUNDLE2_CAPS_SERVER$ 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
308 s> \xa3Dapis\xa1Pexp-http-v2-0001\xa4Hcommands\xa7Eheads\xa2Dargs\xa1Jpubliconly\xf4Kpermissions\x81DpullEknown\xa2Dargs\xa1Enodes\x81HdeadbeefKpermissions\x81DpullFlookup\xa2Dargs\xa1CkeyCfooKpermissions\x81DpullGpushkey\xa2Dargs\xa4CkeyCkeyCnewCnewColdColdInamespaceBnsKpermissions\x81DpushHlistkeys\xa2Dargs\xa1InamespaceBnsKpermissions\x81DpullIbranchmap\xa2Dargs\xa0Kpermissions\x81DpullLcapabilities\xa2Dargs\xa0Kpermissions\x81DpullKcompression\x82\xa1DnameDzstd\xa1DnameDzlibNrawrepoformats\x82LgeneraldeltaHrevlogv1Qframingmediatypes\x81X&application/mercurial-exp-framing-0005GapibaseDapi/Nv1capabilitiesY\x01\xcabatch branchmap $USUAL_BUNDLE2_CAPS_SERVER$ 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
309 309 sending heads command
310 310 s> POST /api/exp-http-v2-0001/ro/heads HTTP/1.1\r\n
311 311 s> Accept-Encoding: identity\r\n
312 s> accept: application/mercurial-exp-framing-0004\r\n
313 s> content-type: application/mercurial-exp-framing-0004\r\n
312 s> accept: application/mercurial-exp-framing-0005\r\n
313 s> content-type: application/mercurial-exp-framing-0005\r\n
314 314 s> content-length: 20\r\n
315 315 s> host: $LOCALIP:$HGPORT\r\n (glob)
316 316 s> user-agent: Mercurial debugwireproto\r\n
317 317 s> \r\n
318 318 s> \x0c\x00\x00\x01\x00\x01\x01\x11\xa1DnameEheads
319 319 s> makefile('rb', None)
320 320 s> HTTP/1.1 200 OK\r\n
321 321 s> Server: testing stub value\r\n
322 322 s> Date: $HTTP_DATE$\r\n
323 s> Content-Type: application/mercurial-exp-framing-0004\r\n
323 s> Content-Type: application/mercurial-exp-framing-0005\r\n
324 324 s> Transfer-Encoding: chunked\r\n
325 325 s> \r\n
326 s> 1e\r\n
327 s> \x16\x00\x00\x01\x00\x02\x012
328 s> \x81T\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00
326 s> 29\r\n
327 s> !\x00\x00\x01\x00\x02\x012
328 s> \xa1FstatusBok\x81T\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00
329 329 s> \r\n
330 received frame(size=22; request=1; stream=2; streamflags=stream-begin; type=command-response; flags=eos)
330 received frame(size=33; request=1; stream=2; streamflags=stream-begin; type=command-response; flags=eos)
331 331 s> 0\r\n
332 332 s> \r\n
333 333 response: [b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00']
334 334
335 335 $ killdaemons.py
@@ -1,72 +1,72 b''
1 1 $ . $TESTDIR/wireprotohelpers.sh
2 2
3 3 $ hg init server
4 4 $ enablehttpv2 server
5 5 $ cd server
6 6 $ hg debugdrawdag << EOF
7 7 > C D
8 8 > |/
9 9 > B
10 10 > |
11 11 > A
12 12 > EOF
13 13
14 14 $ hg up B
15 15 2 files updated, 0 files merged, 0 files removed, 0 files unresolved
16 16 $ hg branch branch1
17 17 marked working directory as branch branch1
18 18 (branches are permanent and global, did you want a bookmark?)
19 19 $ echo b1 > foo
20 20 $ hg -q commit -A -m 'branch 1'
21 21 $ hg up B
22 22 0 files updated, 0 files merged, 1 files removed, 0 files unresolved
23 23 $ hg branch branch2
24 24 marked working directory as branch branch2
25 25 $ echo b2 > foo
26 26 $ hg -q commit -A -m 'branch 2'
27 27
28 28 $ hg log -T '{rev}:{node} {branch} {desc}\n'
29 29 5:224161c7589aa48fa83a48feff5e95b56ae327fc branch2 branch 2
30 30 4:b5faacdfd2633768cb3152336cc0953381266688 branch1 branch 1
31 31 3:be0ef73c17ade3fc89dc41701eb9fc3a91b58282 default D
32 32 2:26805aba1e600a82e93661149f2313866a221a7b default C
33 33 1:112478962961147124edd43549aedd1a335e44bf default B
34 34 0:426bada5c67598ca65036d57d9e4b64b0c1ce7a0 default A
35 35
36 36 $ hg serve -p $HGPORT -d --pid-file hg.pid -E error.log
37 37 $ cat hg.pid > $DAEMON_PIDS
38 38
39 39 No arguments returns something reasonable
40 40
41 41 $ sendhttpv2peer << EOF
42 42 > command branchmap
43 43 > EOF
44 44 creating http peer for wire protocol version 2
45 45 sending branchmap command
46 46 s> POST /api/exp-http-v2-0001/ro/branchmap HTTP/1.1\r\n
47 47 s> Accept-Encoding: identity\r\n
48 s> accept: application/mercurial-exp-framing-0004\r\n
49 s> content-type: application/mercurial-exp-framing-0004\r\n
48 s> accept: application/mercurial-exp-framing-0005\r\n
49 s> content-type: application/mercurial-exp-framing-0005\r\n
50 50 s> content-length: 24\r\n
51 51 s> host: $LOCALIP:$HGPORT\r\n (glob)
52 52 s> user-agent: Mercurial debugwireproto\r\n
53 53 s> \r\n
54 54 s> \x10\x00\x00\x01\x00\x01\x01\x11\xa1DnameIbranchmap
55 55 s> makefile('rb', None)
56 56 s> HTTP/1.1 200 OK\r\n
57 57 s> Server: testing stub value\r\n
58 58 s> Date: $HTTP_DATE$\r\n
59 s> Content-Type: application/mercurial-exp-framing-0004\r\n
59 s> Content-Type: application/mercurial-exp-framing-0005\r\n
60 60 s> Transfer-Encoding: chunked\r\n
61 61 s> \r\n
62 s> 78\r\n
63 s> p\x00\x00\x01\x00\x02\x012
64 s> \xa3Gbranch1\x81T\xb5\xfa\xac\xdf\xd2c7h\xcb1R3l\xc0\x953\x81&f\x88Gbranch2\x81T"Aa\xc7X\x9a\xa4\x8f\xa8:H\xfe\xff^\x95\xb5j\xe3\'\xfcGdefault\x82T&\x80Z\xba\x1e`\n
62 s> 83\r\n
63 s> {\x00\x00\x01\x00\x02\x012
64 s> \xa1FstatusBok\xa3Gbranch1\x81T\xb5\xfa\xac\xdf\xd2c7h\xcb1R3l\xc0\x953\x81&f\x88Gbranch2\x81T"Aa\xc7X\x9a\xa4\x8f\xa8:H\xfe\xff^\x95\xb5j\xe3\'\xfcGdefault\x82T&\x80Z\xba\x1e`\n
65 65 s> \x82\xe96a\x14\x9f#\x13\x86j"\x1a{T\xbe\x0e\xf7<\x17\xad\xe3\xfc\x89\xdcAp\x1e\xb9\xfc:\x91\xb5\x82\x82
66 66 s> \r\n
67 received frame(size=112; request=1; stream=2; streamflags=stream-begin; type=command-response; flags=eos)
67 received frame(size=123; request=1; stream=2; streamflags=stream-begin; type=command-response; flags=eos)
68 68 s> 0\r\n
69 69 s> \r\n
70 70 response: {b'branch1': [b'\xb5\xfa\xac\xdf\xd2c7h\xcb1R3l\xc0\x953\x81&f\x88'], b'branch2': [b'"Aa\xc7X\x9a\xa4\x8f\xa8:H\xfe\xff^\x95\xb5j\xe3\'\xfc'], b'default': [b'&\x80Z\xba\x1e`\n\x82\xe96a\x14\x9f#\x13\x86j"\x1a{', b'\xbe\x0e\xf7<\x17\xad\xe3\xfc\x89\xdcAp\x1e\xb9\xfc:\x91\xb5\x82\x82']}
71 71
72 72 $ cat error.log
@@ -1,247 +1,247 b''
1 1 $ . $TESTDIR/wireprotohelpers.sh
2 2
3 3 $ hg init server
4 4 $ hg -R server serve -p $HGPORT -d --pid-file hg.pid -E error.log
5 5 $ cat hg.pid > $DAEMON_PIDS
6 6
7 7 A normal capabilities request is serviced for version 1
8 8
9 9 $ sendhttpraw << EOF
10 10 > httprequest GET ?cmd=capabilities
11 11 > user-agent: test
12 12 > EOF
13 13 using raw connection to peer
14 14 s> GET /?cmd=capabilities HTTP/1.1\r\n
15 15 s> Accept-Encoding: identity\r\n
16 16 s> user-agent: test\r\n
17 17 s> host: $LOCALIP:$HGPORT\r\n (glob)
18 18 s> \r\n
19 19 s> makefile('rb', None)
20 20 s> HTTP/1.1 200 Script output follows\r\n
21 21 s> Server: testing stub value\r\n
22 22 s> Date: $HTTP_DATE$\r\n
23 23 s> Content-Type: application/mercurial-0.1\r\n
24 24 s> Content-Length: 458\r\n
25 25 s> \r\n
26 26 s> batch branchmap $USUAL_BUNDLE2_CAPS_SERVER$ 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
27 27
28 28 A proper request without the API server enabled returns the legacy response
29 29
30 30 $ sendhttpraw << EOF
31 31 > httprequest GET ?cmd=capabilities
32 32 > user-agent: test
33 33 > x-hgupgrade-1: foo
34 34 > x-hgproto-1: cbor
35 35 > EOF
36 36 using raw connection to peer
37 37 s> GET /?cmd=capabilities HTTP/1.1\r\n
38 38 s> Accept-Encoding: identity\r\n
39 39 s> user-agent: test\r\n
40 40 s> x-hgproto-1: cbor\r\n
41 41 s> x-hgupgrade-1: foo\r\n
42 42 s> host: $LOCALIP:$HGPORT\r\n (glob)
43 43 s> \r\n
44 44 s> makefile('rb', None)
45 45 s> HTTP/1.1 200 Script output follows\r\n
46 46 s> Server: testing stub value\r\n
47 47 s> Date: $HTTP_DATE$\r\n
48 48 s> Content-Type: application/mercurial-0.1\r\n
49 49 s> Content-Length: 458\r\n
50 50 s> \r\n
51 51 s> batch branchmap $USUAL_BUNDLE2_CAPS_SERVER$ 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
52 52
53 53 Restart with just API server enabled. This enables serving the new format.
54 54
55 55 $ killdaemons.py
56 56 $ cat error.log
57 57
58 58 $ cat >> server/.hg/hgrc << EOF
59 59 > [experimental]
60 60 > web.apiserver = true
61 61 > EOF
62 62
63 63 $ hg -R server serve -p $HGPORT -d --pid-file hg.pid -E error.log
64 64 $ cat hg.pid > $DAEMON_PIDS
65 65
66 66 X-HgUpgrade-<N> without CBOR advertisement uses legacy response
67 67
68 68 $ sendhttpraw << EOF
69 69 > httprequest GET ?cmd=capabilities
70 70 > user-agent: test
71 71 > x-hgupgrade-1: foo bar
72 72 > EOF
73 73 using raw connection to peer
74 74 s> GET /?cmd=capabilities HTTP/1.1\r\n
75 75 s> Accept-Encoding: identity\r\n
76 76 s> user-agent: test\r\n
77 77 s> x-hgupgrade-1: foo bar\r\n
78 78 s> host: $LOCALIP:$HGPORT\r\n (glob)
79 79 s> \r\n
80 80 s> makefile('rb', None)
81 81 s> HTTP/1.1 200 Script output follows\r\n
82 82 s> Server: testing stub value\r\n
83 83 s> Date: $HTTP_DATE$\r\n
84 84 s> Content-Type: application/mercurial-0.1\r\n
85 85 s> Content-Length: 458\r\n
86 86 s> \r\n
87 87 s> batch branchmap $USUAL_BUNDLE2_CAPS_SERVER$ 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
88 88
89 89 X-HgUpgrade-<N> without known serialization in X-HgProto-<N> uses legacy response
90 90
91 91 $ sendhttpraw << EOF
92 92 > httprequest GET ?cmd=capabilities
93 93 > user-agent: test
94 94 > x-hgupgrade-1: foo bar
95 95 > x-hgproto-1: some value
96 96 > EOF
97 97 using raw connection to peer
98 98 s> GET /?cmd=capabilities HTTP/1.1\r\n
99 99 s> Accept-Encoding: identity\r\n
100 100 s> user-agent: test\r\n
101 101 s> x-hgproto-1: some value\r\n
102 102 s> x-hgupgrade-1: foo bar\r\n
103 103 s> host: $LOCALIP:$HGPORT\r\n (glob)
104 104 s> \r\n
105 105 s> makefile('rb', None)
106 106 s> HTTP/1.1 200 Script output follows\r\n
107 107 s> Server: testing stub value\r\n
108 108 s> Date: $HTTP_DATE$\r\n
109 109 s> Content-Type: application/mercurial-0.1\r\n
110 110 s> Content-Length: 458\r\n
111 111 s> \r\n
112 112 s> batch branchmap $USUAL_BUNDLE2_CAPS_SERVER$ 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
113 113
114 114 X-HgUpgrade-<N> + X-HgProto-<N> headers trigger new response format
115 115
116 116 $ sendhttpraw << EOF
117 117 > httprequest GET ?cmd=capabilities
118 118 > user-agent: test
119 119 > x-hgupgrade-1: foo bar
120 120 > x-hgproto-1: cbor
121 121 > EOF
122 122 using raw connection to peer
123 123 s> GET /?cmd=capabilities HTTP/1.1\r\n
124 124 s> Accept-Encoding: identity\r\n
125 125 s> user-agent: test\r\n
126 126 s> x-hgproto-1: cbor\r\n
127 127 s> x-hgupgrade-1: foo bar\r\n
128 128 s> host: $LOCALIP:$HGPORT\r\n (glob)
129 129 s> \r\n
130 130 s> makefile('rb', None)
131 131 s> HTTP/1.1 200 OK\r\n
132 132 s> Server: testing stub value\r\n
133 133 s> Date: $HTTP_DATE$\r\n
134 134 s> Content-Type: application/mercurial-cbor\r\n
135 135 s> Content-Length: 496\r\n
136 136 s> \r\n
137 137 s> \xa3Dapis\xa0GapibaseDapi/Nv1capabilitiesY\x01\xcabatch branchmap $USUAL_BUNDLE2_CAPS_SERVER$ 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
138 138 cbor> {b'apibase': b'api/', b'apis': {}, b'v1capabilities': b'batch branchmap $USUAL_BUNDLE2_CAPS_SERVER$ 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'}
139 139
140 140 Restart server to enable HTTPv2
141 141
142 142 $ killdaemons.py
143 143 $ enablehttpv2 server
144 144 $ hg -R server serve -p $HGPORT -d --pid-file hg.pid -E error.log
145 145
146 146 Only requested API services are returned
147 147
148 148 $ sendhttpraw << EOF
149 149 > httprequest GET ?cmd=capabilities
150 150 > user-agent: test
151 151 > x-hgupgrade-1: foo bar
152 152 > x-hgproto-1: cbor
153 153 > EOF
154 154 using raw connection to peer
155 155 s> GET /?cmd=capabilities HTTP/1.1\r\n
156 156 s> Accept-Encoding: identity\r\n
157 157 s> user-agent: test\r\n
158 158 s> x-hgproto-1: cbor\r\n
159 159 s> x-hgupgrade-1: foo bar\r\n
160 160 s> host: $LOCALIP:$HGPORT\r\n (glob)
161 161 s> \r\n
162 162 s> makefile('rb', None)
163 163 s> HTTP/1.1 200 OK\r\n
164 164 s> Server: testing stub value\r\n
165 165 s> Date: $HTTP_DATE$\r\n
166 166 s> Content-Type: application/mercurial-cbor\r\n
167 167 s> Content-Length: 496\r\n
168 168 s> \r\n
169 169 s> \xa3Dapis\xa0GapibaseDapi/Nv1capabilitiesY\x01\xcabatch branchmap $USUAL_BUNDLE2_CAPS_SERVER$ 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
170 170 cbor> {b'apibase': b'api/', b'apis': {}, b'v1capabilities': b'batch branchmap $USUAL_BUNDLE2_CAPS_SERVER$ 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'}
171 171
172 172 Request for HTTPv2 service returns information about it
173 173
174 174 $ sendhttpraw << EOF
175 175 > httprequest GET ?cmd=capabilities
176 176 > user-agent: test
177 177 > x-hgupgrade-1: exp-http-v2-0001 foo bar
178 178 > x-hgproto-1: cbor
179 179 > EOF
180 180 using raw connection to peer
181 181 s> GET /?cmd=capabilities HTTP/1.1\r\n
182 182 s> Accept-Encoding: identity\r\n
183 183 s> user-agent: test\r\n
184 184 s> x-hgproto-1: cbor\r\n
185 185 s> x-hgupgrade-1: exp-http-v2-0001 foo bar\r\n
186 186 s> host: $LOCALIP:$HGPORT\r\n (glob)
187 187 s> \r\n
188 188 s> makefile('rb', None)
189 189 s> HTTP/1.1 200 OK\r\n
190 190 s> Server: testing stub value\r\n
191 191 s> Date: $HTTP_DATE$\r\n
192 192 s> Content-Type: application/mercurial-cbor\r\n
193 193 s> Content-Length: *\r\n (glob)
194 194 s> \r\n
195 s> \xa3Dapis\xa1Pexp-http-v2-0001\xa4Hcommands\xa7Eheads\xa2Dargs\xa1Jpubliconly\xf4Kpermissions\x81DpullEknown\xa2Dargs\xa1Enodes\x81HdeadbeefKpermissions\x81DpullFlookup\xa2Dargs\xa1CkeyCfooKpermissions\x81DpullGpushkey\xa2Dargs\xa4CkeyCkeyCnewCnewColdColdInamespaceBnsKpermissions\x81DpushHlistkeys\xa2Dargs\xa1InamespaceBnsKpermissions\x81DpullIbranchmap\xa2Dargs\xa0Kpermissions\x81DpullLcapabilities\xa2Dargs\xa0Kpermissions\x81DpullKcompression\x82\xa1DnameDzstd\xa1DnameDzlibNrawrepoformats\x82LgeneraldeltaHrevlogv1Qframingmediatypes\x81X&application/mercurial-exp-framing-0004GapibaseDapi/Nv1capabilitiesY\x01\xcabatch branchmap $USUAL_BUNDLE2_CAPS_SERVER$ 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
196 cbor> {b'apibase': b'api/', b'apis': {b'exp-http-v2-0001': {b'commands': {b'branchmap': {b'args': {}, b'permissions': [b'pull']}, b'capabilities': {b'args': {}, b'permissions': [b'pull']}, b'heads': {b'args': {b'publiconly': False}, b'permissions': [b'pull']}, b'known': {b'args': {b'nodes': [b'deadbeef']}, b'permissions': [b'pull']}, b'listkeys': {b'args': {b'namespace': b'ns'}, b'permissions': [b'pull']}, b'lookup': {b'args': {b'key': b'foo'}, b'permissions': [b'pull']}, b'pushkey': {b'args': {b'key': b'key', b'namespace': b'ns', b'new': b'new', b'old': b'old'}, b'permissions': [b'push']}}, b'compression': [{b'name': b'zstd'}, {b'name': b'zlib'}], b'framingmediatypes': [b'application/mercurial-exp-framing-0004'], b'rawrepoformats': [b'generaldelta', b'revlogv1']}}, b'v1capabilities': b'batch branchmap $USUAL_BUNDLE2_CAPS_SERVER$ 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'}
195 s> \xa3Dapis\xa1Pexp-http-v2-0001\xa4Hcommands\xa7Eheads\xa2Dargs\xa1Jpubliconly\xf4Kpermissions\x81DpullEknown\xa2Dargs\xa1Enodes\x81HdeadbeefKpermissions\x81DpullFlookup\xa2Dargs\xa1CkeyCfooKpermissions\x81DpullGpushkey\xa2Dargs\xa4CkeyCkeyCnewCnewColdColdInamespaceBnsKpermissions\x81DpushHlistkeys\xa2Dargs\xa1InamespaceBnsKpermissions\x81DpullIbranchmap\xa2Dargs\xa0Kpermissions\x81DpullLcapabilities\xa2Dargs\xa0Kpermissions\x81DpullKcompression\x82\xa1DnameDzstd\xa1DnameDzlibNrawrepoformats\x82LgeneraldeltaHrevlogv1Qframingmediatypes\x81X&application/mercurial-exp-framing-0005GapibaseDapi/Nv1capabilitiesY\x01\xcabatch branchmap $USUAL_BUNDLE2_CAPS_SERVER$ 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
196 cbor> {b'apibase': b'api/', b'apis': {b'exp-http-v2-0001': {b'commands': {b'branchmap': {b'args': {}, b'permissions': [b'pull']}, b'capabilities': {b'args': {}, b'permissions': [b'pull']}, b'heads': {b'args': {b'publiconly': False}, b'permissions': [b'pull']}, b'known': {b'args': {b'nodes': [b'deadbeef']}, b'permissions': [b'pull']}, b'listkeys': {b'args': {b'namespace': b'ns'}, b'permissions': [b'pull']}, b'lookup': {b'args': {b'key': b'foo'}, b'permissions': [b'pull']}, b'pushkey': {b'args': {b'key': b'key', b'namespace': b'ns', b'new': b'new', b'old': b'old'}, b'permissions': [b'push']}}, b'compression': [{b'name': b'zstd'}, {b'name': b'zlib'}], b'framingmediatypes': [b'application/mercurial-exp-framing-0005'], b'rawrepoformats': [b'generaldelta', b'revlogv1']}}, b'v1capabilities': b'batch branchmap $USUAL_BUNDLE2_CAPS_SERVER$ 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'}
197 197
198 198 capabilities command returns expected info
199 199
200 200 $ sendhttpv2peerhandshake << EOF
201 201 > command capabilities
202 202 > EOF
203 203 creating http peer for wire protocol version 2
204 204 s> GET /?cmd=capabilities HTTP/1.1\r\n
205 205 s> Accept-Encoding: identity\r\n
206 206 s> vary: X-HgProto-1,X-HgUpgrade-1\r\n
207 207 s> x-hgproto-1: cbor\r\n
208 208 s> x-hgupgrade-1: exp-http-v2-0001\r\n
209 209 s> accept: application/mercurial-0.1\r\n
210 210 s> host: $LOCALIP:$HGPORT\r\n (glob)
211 211 s> user-agent: Mercurial debugwireproto\r\n
212 212 s> \r\n
213 213 s> makefile('rb', None)
214 214 s> HTTP/1.1 200 OK\r\n
215 215 s> Server: testing stub value\r\n
216 216 s> Date: $HTTP_DATE$\r\n
217 217 s> Content-Type: application/mercurial-cbor\r\n
218 218 s> Content-Length: *\r\n (glob)
219 219 s> \r\n
220 s> \xa3Dapis\xa1Pexp-http-v2-0001\xa4Hcommands\xa7Eheads\xa2Dargs\xa1Jpubliconly\xf4Kpermissions\x81DpullEknown\xa2Dargs\xa1Enodes\x81HdeadbeefKpermissions\x81DpullFlookup\xa2Dargs\xa1CkeyCfooKpermissions\x81DpullGpushkey\xa2Dargs\xa4CkeyCkeyCnewCnewColdColdInamespaceBnsKpermissions\x81DpushHlistkeys\xa2Dargs\xa1InamespaceBnsKpermissions\x81DpullIbranchmap\xa2Dargs\xa0Kpermissions\x81DpullLcapabilities\xa2Dargs\xa0Kpermissions\x81DpullKcompression\x82\xa1DnameDzstd\xa1DnameDzlibNrawrepoformats\x82LgeneraldeltaHrevlogv1Qframingmediatypes\x81X&application/mercurial-exp-framing-0004GapibaseDapi/Nv1capabilitiesY\x01\xcabatch branchmap $USUAL_BUNDLE2_CAPS_SERVER$ 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
220 s> \xa3Dapis\xa1Pexp-http-v2-0001\xa4Hcommands\xa7Eheads\xa2Dargs\xa1Jpubliconly\xf4Kpermissions\x81DpullEknown\xa2Dargs\xa1Enodes\x81HdeadbeefKpermissions\x81DpullFlookup\xa2Dargs\xa1CkeyCfooKpermissions\x81DpullGpushkey\xa2Dargs\xa4CkeyCkeyCnewCnewColdColdInamespaceBnsKpermissions\x81DpushHlistkeys\xa2Dargs\xa1InamespaceBnsKpermissions\x81DpullIbranchmap\xa2Dargs\xa0Kpermissions\x81DpullLcapabilities\xa2Dargs\xa0Kpermissions\x81DpullKcompression\x82\xa1DnameDzstd\xa1DnameDzlibNrawrepoformats\x82LgeneraldeltaHrevlogv1Qframingmediatypes\x81X&application/mercurial-exp-framing-0005GapibaseDapi/Nv1capabilitiesY\x01\xcabatch branchmap $USUAL_BUNDLE2_CAPS_SERVER$ 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
221 221 sending capabilities command
222 222 s> POST /api/exp-http-v2-0001/ro/capabilities HTTP/1.1\r\n
223 223 s> Accept-Encoding: identity\r\n
224 224 s> *\r\n (glob)
225 s> content-type: application/mercurial-exp-framing-0004\r\n
225 s> content-type: application/mercurial-exp-framing-0005\r\n
226 226 s> content-length: 27\r\n
227 227 s> host: $LOCALIP:$HGPORT\r\n (glob)
228 228 s> user-agent: Mercurial debugwireproto\r\n
229 229 s> \r\n
230 230 s> \x13\x00\x00\x01\x00\x01\x01\x11\xa1DnameLcapabilities
231 231 s> makefile('rb', None)
232 232 s> HTTP/1.1 200 OK\r\n
233 233 s> Server: testing stub value\r\n
234 234 s> Date: $HTTP_DATE$\r\n
235 s> Content-Type: application/mercurial-exp-framing-0004\r\n
235 s> Content-Type: application/mercurial-exp-framing-0005\r\n
236 236 s> Transfer-Encoding: chunked\r\n
237 237 s> \r\n
238 s> 1d7\r\n
239 s> \xcf\x01\x00\x01\x00\x02\x012
240 s> \xa4Hcommands\xa7Eheads\xa2Dargs\xa1Jpubliconly\xf4Kpermissions\x81DpullEknown\xa2Dargs\xa1Enodes\x81HdeadbeefKpermissions\x81DpullFlookup\xa2Dargs\xa1CkeyCfooKpermissions\x81DpullGpushkey\xa2Dargs\xa4CkeyCkeyCnewCnewColdColdInamespaceBnsKpermissions\x81DpushHlistkeys\xa2Dargs\xa1InamespaceBnsKpermissions\x81DpullIbranchmap\xa2Dargs\xa0Kpermissions\x81DpullLcapabilities\xa2Dargs\xa0Kpermissions\x81DpullKcompression\x82\xa1DnameDzstd\xa1DnameDzlibNrawrepoformats\x82LgeneraldeltaHrevlogv1Qframingmediatypes\x81X&application/mercurial-exp-framing-0004
238 s> 1e2\r\n
239 s> \xda\x01\x00\x01\x00\x02\x012
240 s> \xa1FstatusBok\xa4Hcommands\xa7Eheads\xa2Dargs\xa1Jpubliconly\xf4Kpermissions\x81DpullEknown\xa2Dargs\xa1Enodes\x81HdeadbeefKpermissions\x81DpullFlookup\xa2Dargs\xa1CkeyCfooKpermissions\x81DpullGpushkey\xa2Dargs\xa4CkeyCkeyCnewCnewColdColdInamespaceBnsKpermissions\x81DpushHlistkeys\xa2Dargs\xa1InamespaceBnsKpermissions\x81DpullIbranchmap\xa2Dargs\xa0Kpermissions\x81DpullLcapabilities\xa2Dargs\xa0Kpermissions\x81DpullKcompression\x82\xa1DnameDzstd\xa1DnameDzlibNrawrepoformats\x82LgeneraldeltaHrevlogv1Qframingmediatypes\x81X&application/mercurial-exp-framing-0005
241 241 s> \r\n
242 received frame(size=463; request=1; stream=2; streamflags=stream-begin; type=command-response; flags=eos)
242 received frame(size=474; request=1; stream=2; streamflags=stream-begin; type=command-response; flags=eos)
243 243 s> 0\r\n
244 244 s> \r\n
245 response: [{b'commands': {b'branchmap': {b'args': {}, b'permissions': [b'pull']}, b'capabilities': {b'args': {}, b'permissions': [b'pull']}, b'heads': {b'args': {b'publiconly': False}, b'permissions': [b'pull']}, b'known': {b'args': {b'nodes': [b'deadbeef']}, b'permissions': [b'pull']}, b'listkeys': {b'args': {b'namespace': b'ns'}, b'permissions': [b'pull']}, b'lookup': {b'args': {b'key': b'foo'}, b'permissions': [b'pull']}, b'pushkey': {b'args': {b'key': b'key', b'namespace': b'ns', b'new': b'new', b'old': b'old'}, b'permissions': [b'push']}}, b'compression': [{b'name': b'zstd'}, {b'name': b'zlib'}], b'framingmediatypes': [b'application/mercurial-exp-framing-0004'], b'rawrepoformats': [b'generaldelta', b'revlogv1']}]
245 response: [{b'status': b'ok'}, {b'commands': {b'branchmap': {b'args': {}, b'permissions': [b'pull']}, b'capabilities': {b'args': {}, b'permissions': [b'pull']}, b'heads': {b'args': {b'publiconly': False}, b'permissions': [b'pull']}, b'known': {b'args': {b'nodes': [b'deadbeef']}, b'permissions': [b'pull']}, b'listkeys': {b'args': {b'namespace': b'ns'}, b'permissions': [b'pull']}, b'lookup': {b'args': {b'key': b'foo'}, b'permissions': [b'pull']}, b'pushkey': {b'args': {b'key': b'key', b'namespace': b'ns', b'new': b'new', b'old': b'old'}, b'permissions': [b'push']}}, b'compression': [{b'name': b'zstd'}, {b'name': b'zlib'}], b'framingmediatypes': [b'application/mercurial-exp-framing-0005'], b'rawrepoformats': [b'generaldelta', b'revlogv1']}]
246 246
247 247 $ cat error.log
@@ -1,96 +1,96 b''
1 1 $ . $TESTDIR/wireprotohelpers.sh
2 2
3 3 $ hg init server
4 4 $ enablehttpv2 server
5 5 $ cd server
6 6 $ hg debugdrawdag << EOF
7 7 > H I J
8 8 > | | |
9 9 > E F G
10 10 > | |/
11 11 > C D
12 12 > |/
13 13 > B
14 14 > |
15 15 > A
16 16 > EOF
17 17
18 18 $ hg phase --force --secret J
19 19 $ hg phase --public E
20 20
21 21 $ hg log -r 'E + H + I + G + J' -T '{rev}:{node} {desc} {phase}\n'
22 22 4:78d2dca436b2f5b188ac267e29b81e07266d38fc E public
23 23 7:ae492e36b0c8339ffaf328d00b85b4525de1165e H draft
24 24 8:1d6f6b91d44aaba6d5e580bc30a9948530dbe00b I draft
25 25 6:29446d2dc5419c5f97447a8bc062e4cc328bf241 G draft
26 26 9:dec04b246d7cbb670c6689806c05ad17c835284e J secret
27 27
28 28 $ hg serve -p $HGPORT -d --pid-file hg.pid -E error.log
29 29 $ cat hg.pid > $DAEMON_PIDS
30 30
31 31 All non-secret heads returned by default
32 32
33 33 $ sendhttpv2peer << EOF
34 34 > command heads
35 35 > EOF
36 36 creating http peer for wire protocol version 2
37 37 sending heads command
38 38 s> POST /api/exp-http-v2-0001/ro/heads HTTP/1.1\r\n
39 39 s> Accept-Encoding: identity\r\n
40 s> accept: application/mercurial-exp-framing-0004\r\n
41 s> content-type: application/mercurial-exp-framing-0004\r\n
40 s> accept: application/mercurial-exp-framing-0005\r\n
41 s> content-type: application/mercurial-exp-framing-0005\r\n
42 42 s> content-length: 20\r\n
43 43 s> host: $LOCALIP:$HGPORT\r\n (glob)
44 44 s> user-agent: Mercurial debugwireproto\r\n
45 45 s> \r\n
46 46 s> \x0c\x00\x00\x01\x00\x01\x01\x11\xa1DnameEheads
47 47 s> makefile('rb', None)
48 48 s> HTTP/1.1 200 OK\r\n
49 49 s> Server: testing stub value\r\n
50 50 s> Date: $HTTP_DATE$\r\n
51 s> Content-Type: application/mercurial-exp-framing-0004\r\n
51 s> Content-Type: application/mercurial-exp-framing-0005\r\n
52 52 s> Transfer-Encoding: chunked\r\n
53 53 s> \r\n
54 s> 48\r\n
55 s> @\x00\x00\x01\x00\x02\x012
56 s> \x83T\x1dok\x91\xd4J\xab\xa6\xd5\xe5\x80\xbc0\xa9\x94\x850\xdb\xe0\x0bT\xaeI.6\xb0\xc83\x9f\xfa\xf3(\xd0\x0b\x85\xb4R]\xe1\x16^T)Dm-\xc5A\x9c_\x97Dz\x8b\xc0b\xe4\xcc2\x8b\xf2A
54 s> 53\r\n
55 s> K\x00\x00\x01\x00\x02\x012
56 s> \xa1FstatusBok\x83T\x1dok\x91\xd4J\xab\xa6\xd5\xe5\x80\xbc0\xa9\x94\x850\xdb\xe0\x0bT\xaeI.6\xb0\xc83\x9f\xfa\xf3(\xd0\x0b\x85\xb4R]\xe1\x16^T)Dm-\xc5A\x9c_\x97Dz\x8b\xc0b\xe4\xcc2\x8b\xf2A
57 57 s> \r\n
58 received frame(size=64; request=1; stream=2; streamflags=stream-begin; type=command-response; flags=eos)
58 received frame(size=75; request=1; stream=2; streamflags=stream-begin; type=command-response; flags=eos)
59 59 s> 0\r\n
60 60 s> \r\n
61 61 response: [b'\x1dok\x91\xd4J\xab\xa6\xd5\xe5\x80\xbc0\xa9\x94\x850\xdb\xe0\x0b', b'\xaeI.6\xb0\xc83\x9f\xfa\xf3(\xd0\x0b\x85\xb4R]\xe1\x16^', b')Dm-\xc5A\x9c_\x97Dz\x8b\xc0b\xe4\xcc2\x8b\xf2A']
62 62
63 63 Requesting just the public heads works
64 64
65 65 $ sendhttpv2peer << EOF
66 66 > command heads
67 67 > publiconly 1
68 68 > EOF
69 69 creating http peer for wire protocol version 2
70 70 sending heads command
71 71 s> POST /api/exp-http-v2-0001/ro/heads HTTP/1.1\r\n
72 72 s> Accept-Encoding: identity\r\n
73 s> accept: application/mercurial-exp-framing-0004\r\n
74 s> content-type: application/mercurial-exp-framing-0004\r\n
73 s> accept: application/mercurial-exp-framing-0005\r\n
74 s> content-type: application/mercurial-exp-framing-0005\r\n
75 75 s> content-length: 39\r\n
76 76 s> host: $LOCALIP:$HGPORT\r\n (glob)
77 77 s> user-agent: Mercurial debugwireproto\r\n
78 78 s> \r\n
79 79 s> \x1f\x00\x00\x01\x00\x01\x01\x11\xa2Dargs\xa1JpubliconlyA1DnameEheads
80 80 s> makefile('rb', None)
81 81 s> HTTP/1.1 200 OK\r\n
82 82 s> Server: testing stub value\r\n
83 83 s> Date: $HTTP_DATE$\r\n
84 s> Content-Type: application/mercurial-exp-framing-0004\r\n
84 s> Content-Type: application/mercurial-exp-framing-0005\r\n
85 85 s> Transfer-Encoding: chunked\r\n
86 86 s> \r\n
87 s> 1e\r\n
88 s> \x16\x00\x00\x01\x00\x02\x012
89 s> \x81Tx\xd2\xdc\xa46\xb2\xf5\xb1\x88\xac&~)\xb8\x1e\x07&m8\xfc
87 s> 29\r\n
88 s> !\x00\x00\x01\x00\x02\x012
89 s> \xa1FstatusBok\x81Tx\xd2\xdc\xa46\xb2\xf5\xb1\x88\xac&~)\xb8\x1e\x07&m8\xfc
90 90 s> \r\n
91 received frame(size=22; request=1; stream=2; streamflags=stream-begin; type=command-response; flags=eos)
91 received frame(size=33; request=1; stream=2; streamflags=stream-begin; type=command-response; flags=eos)
92 92 s> 0\r\n
93 93 s> \r\n
94 94 response: [b'x\xd2\xdc\xa46\xb2\xf5\xb1\x88\xac&~)\xb8\x1e\x07&m8\xfc']
95 95
96 96 $ cat error.log
@@ -1,121 +1,121 b''
1 1 $ . $TESTDIR/wireprotohelpers.sh
2 2
3 3 $ hg init server
4 4 $ enablehttpv2 server
5 5 $ cd server
6 6 $ hg debugdrawdag << EOF
7 7 > C D
8 8 > |/
9 9 > B
10 10 > |
11 11 > A
12 12 > EOF
13 13
14 14 $ hg log -T '{rev}:{node} {desc}\n'
15 15 3:be0ef73c17ade3fc89dc41701eb9fc3a91b58282 D
16 16 2:26805aba1e600a82e93661149f2313866a221a7b C
17 17 1:112478962961147124edd43549aedd1a335e44bf B
18 18 0:426bada5c67598ca65036d57d9e4b64b0c1ce7a0 A
19 19
20 20 $ hg serve -p $HGPORT -d --pid-file hg.pid -E error.log
21 21 $ cat hg.pid > $DAEMON_PIDS
22 22
23 23 No arguments returns something reasonable
24 24
25 25 $ sendhttpv2peer << EOF
26 26 > command known
27 27 > EOF
28 28 creating http peer for wire protocol version 2
29 29 sending known command
30 30 s> POST /api/exp-http-v2-0001/ro/known HTTP/1.1\r\n
31 31 s> Accept-Encoding: identity\r\n
32 s> accept: application/mercurial-exp-framing-0004\r\n
33 s> content-type: application/mercurial-exp-framing-0004\r\n
32 s> accept: application/mercurial-exp-framing-0005\r\n
33 s> content-type: application/mercurial-exp-framing-0005\r\n
34 34 s> content-length: 20\r\n
35 35 s> host: $LOCALIP:$HGPORT\r\n (glob)
36 36 s> user-agent: Mercurial debugwireproto\r\n
37 37 s> \r\n
38 38 s> \x0c\x00\x00\x01\x00\x01\x01\x11\xa1DnameEknown
39 39 s> makefile('rb', None)
40 40 s> HTTP/1.1 200 OK\r\n
41 41 s> Server: testing stub value\r\n
42 42 s> Date: $HTTP_DATE$\r\n
43 s> Content-Type: application/mercurial-exp-framing-0004\r\n
43 s> Content-Type: application/mercurial-exp-framing-0005\r\n
44 44 s> Transfer-Encoding: chunked\r\n
45 45 s> \r\n
46 s> 9\r\n
47 s> \x01\x00\x00\x01\x00\x02\x012
48 s> @
46 s> 14\r\n
47 s> \x0c\x00\x00\x01\x00\x02\x012
48 s> \xa1FstatusBok@
49 49 s> \r\n
50 received frame(size=1; request=1; stream=2; streamflags=stream-begin; type=command-response; flags=eos)
50 received frame(size=12; request=1; stream=2; streamflags=stream-begin; type=command-response; flags=eos)
51 51 s> 0\r\n
52 52 s> \r\n
53 53 response: []
54 54
55 55 Single known node works
56 56
57 57 $ sendhttpv2peer << EOF
58 58 > command known
59 59 > nodes eval:[b'\x42\x6b\xad\xa5\xc6\x75\x98\xca\x65\x03\x6d\x57\xd9\xe4\xb6\x4b\x0c\x1c\xe7\xa0']
60 60 > EOF
61 61 creating http peer for wire protocol version 2
62 62 sending known command
63 63 s> POST /api/exp-http-v2-0001/ro/known HTTP/1.1\r\n
64 64 s> Accept-Encoding: identity\r\n
65 s> accept: application/mercurial-exp-framing-0004\r\n
66 s> content-type: application/mercurial-exp-framing-0004\r\n
65 s> accept: application/mercurial-exp-framing-0005\r\n
66 s> content-type: application/mercurial-exp-framing-0005\r\n
67 67 s> content-length: 54\r\n
68 68 s> host: $LOCALIP:$HGPORT\r\n (glob)
69 69 s> user-agent: Mercurial debugwireproto\r\n
70 70 s> \r\n
71 71 s> .\x00\x00\x01\x00\x01\x01\x11\xa2Dargs\xa1Enodes\x81TBk\xad\xa5\xc6u\x98\xcae\x03mW\xd9\xe4\xb6K\x0c\x1c\xe7\xa0DnameEknown
72 72 s> makefile('rb', None)
73 73 s> HTTP/1.1 200 OK\r\n
74 74 s> Server: testing stub value\r\n
75 75 s> Date: $HTTP_DATE$\r\n
76 s> Content-Type: application/mercurial-exp-framing-0004\r\n
76 s> Content-Type: application/mercurial-exp-framing-0005\r\n
77 77 s> Transfer-Encoding: chunked\r\n
78 78 s> \r\n
79 s> a\r\n
80 s> \x02\x00\x00\x01\x00\x02\x012
81 s> A1
79 s> 15\r\n
80 s> \r\x00\x00\x01\x00\x02\x012
81 s> \xa1FstatusBokA1
82 82 s> \r\n
83 received frame(size=2; request=1; stream=2; streamflags=stream-begin; type=command-response; flags=eos)
83 received frame(size=13; request=1; stream=2; streamflags=stream-begin; type=command-response; flags=eos)
84 84 s> 0\r\n
85 85 s> \r\n
86 86 response: [True]
87 87
88 88 Multiple nodes works
89 89
90 90 $ sendhttpv2peer << EOF
91 91 > command known
92 92 > nodes eval:[b'\x42\x6b\xad\xa5\xc6\x75\x98\xca\x65\x03\x6d\x57\xd9\xe4\xb6\x4b\x0c\x1c\xe7\xa0', b'00000000000000000000', b'\x11\x24\x78\x96\x29\x61\x14\x71\x24\xed\xd4\x35\x49\xae\xdd\x1a\x33\x5e\x44\xbf']
93 93 > EOF
94 94 creating http peer for wire protocol version 2
95 95 sending known command
96 96 s> POST /api/exp-http-v2-0001/ro/known HTTP/1.1\r\n
97 97 s> Accept-Encoding: identity\r\n
98 s> accept: application/mercurial-exp-framing-0004\r\n
99 s> content-type: application/mercurial-exp-framing-0004\r\n
98 s> accept: application/mercurial-exp-framing-0005\r\n
99 s> content-type: application/mercurial-exp-framing-0005\r\n
100 100 s> content-length: 96\r\n
101 101 s> host: $LOCALIP:$HGPORT\r\n (glob)
102 102 s> user-agent: Mercurial debugwireproto\r\n
103 103 s> \r\n
104 104 s> X\x00\x00\x01\x00\x01\x01\x11\xa2Dargs\xa1Enodes\x83TBk\xad\xa5\xc6u\x98\xcae\x03mW\xd9\xe4\xb6K\x0c\x1c\xe7\xa0T00000000000000000000T\x11$x\x96)a\x14q$\xed\xd45I\xae\xdd\x1a3^D\xbfDnameEknown
105 105 s> makefile('rb', None)
106 106 s> HTTP/1.1 200 OK\r\n
107 107 s> Server: testing stub value\r\n
108 108 s> Date: $HTTP_DATE$\r\n
109 s> Content-Type: application/mercurial-exp-framing-0004\r\n
109 s> Content-Type: application/mercurial-exp-framing-0005\r\n
110 110 s> Transfer-Encoding: chunked\r\n
111 111 s> \r\n
112 s> c\r\n
113 s> \x04\x00\x00\x01\x00\x02\x012
114 s> C101
112 s> 17\r\n
113 s> \x0f\x00\x00\x01\x00\x02\x012
114 s> \xa1FstatusBokC101
115 115 s> \r\n
116 received frame(size=4; request=1; stream=2; streamflags=stream-begin; type=command-response; flags=eos)
116 received frame(size=15; request=1; stream=2; streamflags=stream-begin; type=command-response; flags=eos)
117 117 s> 0\r\n
118 118 s> \r\n
119 119 response: [True, False, True]
120 120
121 121 $ cat error.log
@@ -1,125 +1,125 b''
1 1 $ . $TESTDIR/wireprotohelpers.sh
2 2
3 3 $ hg init server
4 4 $ enablehttpv2 server
5 5 $ cd server
6 6 $ hg debugdrawdag << EOF
7 7 > C D
8 8 > |/
9 9 > B
10 10 > |
11 11 > A
12 12 > EOF
13 13
14 14 $ hg phase --public -r C
15 15 $ hg book -r C @
16 16
17 17 $ hg log -T '{rev}:{node} {desc}\n'
18 18 3:be0ef73c17ade3fc89dc41701eb9fc3a91b58282 D
19 19 2:26805aba1e600a82e93661149f2313866a221a7b C
20 20 1:112478962961147124edd43549aedd1a335e44bf B
21 21 0:426bada5c67598ca65036d57d9e4b64b0c1ce7a0 A
22 22
23 23 $ hg serve -p $HGPORT -d --pid-file hg.pid -E error.log
24 24 $ cat hg.pid > $DAEMON_PIDS
25 25
26 26 Request for namespaces works
27 27
28 28 $ sendhttpv2peer << EOF
29 29 > command listkeys
30 30 > namespace namespaces
31 31 > EOF
32 32 creating http peer for wire protocol version 2
33 33 sending listkeys command
34 34 s> POST /api/exp-http-v2-0001/ro/listkeys HTTP/1.1\r\n
35 35 s> Accept-Encoding: identity\r\n
36 s> accept: application/mercurial-exp-framing-0004\r\n
37 s> content-type: application/mercurial-exp-framing-0004\r\n
36 s> accept: application/mercurial-exp-framing-0005\r\n
37 s> content-type: application/mercurial-exp-framing-0005\r\n
38 38 s> content-length: 50\r\n
39 39 s> host: $LOCALIP:$HGPORT\r\n (glob)
40 40 s> user-agent: Mercurial debugwireproto\r\n
41 41 s> \r\n
42 42 s> *\x00\x00\x01\x00\x01\x01\x11\xa2Dargs\xa1InamespaceJnamespacesDnameHlistkeys
43 43 s> makefile('rb', None)
44 44 s> HTTP/1.1 200 OK\r\n
45 45 s> Server: testing stub value\r\n
46 46 s> Date: $HTTP_DATE$\r\n
47 s> Content-Type: application/mercurial-exp-framing-0004\r\n
47 s> Content-Type: application/mercurial-exp-framing-0005\r\n
48 48 s> Transfer-Encoding: chunked\r\n
49 49 s> \r\n
50 s> 28\r\n
51 s> \x00\x00\x01\x00\x02\x012
52 s> \xa3Fphases@Ibookmarks@Jnamespaces@
50 s> 33\r\n
51 s> +\x00\x00\x01\x00\x02\x012
52 s> \xa1FstatusBok\xa3Fphases@Ibookmarks@Jnamespaces@
53 53 s> \r\n
54 received frame(size=32; request=1; stream=2; streamflags=stream-begin; type=command-response; flags=eos)
54 received frame(size=43; request=1; stream=2; streamflags=stream-begin; type=command-response; flags=eos)
55 55 s> 0\r\n
56 56 s> \r\n
57 57 response: {b'bookmarks': b'', b'namespaces': b'', b'phases': b''}
58 58
59 59 Request for phases works
60 60
61 61 $ sendhttpv2peer << EOF
62 62 > command listkeys
63 63 > namespace phases
64 64 > EOF
65 65 creating http peer for wire protocol version 2
66 66 sending listkeys command
67 67 s> POST /api/exp-http-v2-0001/ro/listkeys HTTP/1.1\r\n
68 68 s> Accept-Encoding: identity\r\n
69 s> accept: application/mercurial-exp-framing-0004\r\n
70 s> content-type: application/mercurial-exp-framing-0004\r\n
69 s> accept: application/mercurial-exp-framing-0005\r\n
70 s> content-type: application/mercurial-exp-framing-0005\r\n
71 71 s> content-length: 46\r\n
72 72 s> host: $LOCALIP:$HGPORT\r\n (glob)
73 73 s> user-agent: Mercurial debugwireproto\r\n
74 74 s> \r\n
75 75 s> &\x00\x00\x01\x00\x01\x01\x11\xa2Dargs\xa1InamespaceFphasesDnameHlistkeys
76 76 s> makefile('rb', None)
77 77 s> HTTP/1.1 200 OK\r\n
78 78 s> Server: testing stub value\r\n
79 79 s> Date: $HTTP_DATE$\r\n
80 s> Content-Type: application/mercurial-exp-framing-0004\r\n
80 s> Content-Type: application/mercurial-exp-framing-0005\r\n
81 81 s> Transfer-Encoding: chunked\r\n
82 82 s> \r\n
83 s> 45\r\n
84 s> =\x00\x00\x01\x00\x02\x012
85 s> \xa2JpublishingDTrueX(be0ef73c17ade3fc89dc41701eb9fc3a91b58282A1
83 s> 50\r\n
84 s> H\x00\x00\x01\x00\x02\x012
85 s> \xa1FstatusBok\xa2JpublishingDTrueX(be0ef73c17ade3fc89dc41701eb9fc3a91b58282A1
86 86 s> \r\n
87 received frame(size=61; request=1; stream=2; streamflags=stream-begin; type=command-response; flags=eos)
87 received frame(size=72; request=1; stream=2; streamflags=stream-begin; type=command-response; flags=eos)
88 88 s> 0\r\n
89 89 s> \r\n
90 90 response: {b'be0ef73c17ade3fc89dc41701eb9fc3a91b58282': b'1', b'publishing': b'True'}
91 91
92 92 Request for bookmarks works
93 93
94 94 $ sendhttpv2peer << EOF
95 95 > command listkeys
96 96 > namespace bookmarks
97 97 > EOF
98 98 creating http peer for wire protocol version 2
99 99 sending listkeys command
100 100 s> POST /api/exp-http-v2-0001/ro/listkeys HTTP/1.1\r\n
101 101 s> Accept-Encoding: identity\r\n
102 s> accept: application/mercurial-exp-framing-0004\r\n
103 s> content-type: application/mercurial-exp-framing-0004\r\n
102 s> accept: application/mercurial-exp-framing-0005\r\n
103 s> content-type: application/mercurial-exp-framing-0005\r\n
104 104 s> content-length: 49\r\n
105 105 s> host: $LOCALIP:$HGPORT\r\n (glob)
106 106 s> user-agent: Mercurial debugwireproto\r\n
107 107 s> \r\n
108 108 s> )\x00\x00\x01\x00\x01\x01\x11\xa2Dargs\xa1InamespaceIbookmarksDnameHlistkeys
109 109 s> makefile('rb', None)
110 110 s> HTTP/1.1 200 OK\r\n
111 111 s> Server: testing stub value\r\n
112 112 s> Date: $HTTP_DATE$\r\n
113 s> Content-Type: application/mercurial-exp-framing-0004\r\n
113 s> Content-Type: application/mercurial-exp-framing-0005\r\n
114 114 s> Transfer-Encoding: chunked\r\n
115 115 s> \r\n
116 s> 35\r\n
117 s> -\x00\x00\x01\x00\x02\x012
118 s> \xa1A@X(26805aba1e600a82e93661149f2313866a221a7b
116 s> 40\r\n
117 s> 8\x00\x00\x01\x00\x02\x012
118 s> \xa1FstatusBok\xa1A@X(26805aba1e600a82e93661149f2313866a221a7b
119 119 s> \r\n
120 received frame(size=45; request=1; stream=2; streamflags=stream-begin; type=command-response; flags=eos)
120 received frame(size=56; request=1; stream=2; streamflags=stream-begin; type=command-response; flags=eos)
121 121 s> 0\r\n
122 122 s> \r\n
123 123 response: {b'@': b'26805aba1e600a82e93661149f2313866a221a7b'}
124 124
125 125 $ cat error.log
@@ -1,55 +1,55 b''
1 1 $ . $TESTDIR/wireprotohelpers.sh
2 2
3 3 $ hg init server
4 4 $ enablehttpv2 server
5 5 $ cd server
6 6 $ cat >> .hg/hgrc << EOF
7 7 > [web]
8 8 > push_ssl = false
9 9 > allow-push = *
10 10 > EOF
11 11 $ hg debugdrawdag << EOF
12 12 > C D
13 13 > |/
14 14 > B
15 15 > |
16 16 > A
17 17 > EOF
18 18
19 19 $ hg serve -p $HGPORT -d --pid-file hg.pid -E error.log
20 20 $ cat hg.pid > $DAEMON_PIDS
21 21
22 22 lookup for known node works
23 23
24 24 $ sendhttpv2peer << EOF
25 25 > command lookup
26 26 > key 426bada5c67598ca65036d57d9e4b64b0c1ce7a0
27 27 > EOF
28 28 creating http peer for wire protocol version 2
29 29 sending lookup command
30 30 s> *\r\n (glob)
31 31 s> Accept-Encoding: identity\r\n
32 s> accept: application/mercurial-exp-framing-0004\r\n
33 s> content-type: application/mercurial-exp-framing-0004\r\n
32 s> accept: application/mercurial-exp-framing-0005\r\n
33 s> content-type: application/mercurial-exp-framing-0005\r\n
34 34 s> content-length: 73\r\n
35 35 s> host: $LOCALIP:$HGPORT\r\n (glob)
36 36 s> user-agent: Mercurial debugwireproto\r\n
37 37 s> \r\n
38 38 s> A\x00\x00\x01\x00\x01\x01\x11\xa2Dargs\xa1CkeyX(426bada5c67598ca65036d57d9e4b64b0c1ce7a0DnameFlookup
39 39 s> makefile('rb', None)
40 40 s> HTTP/1.1 200 OK\r\n
41 41 s> Server: testing stub value\r\n
42 42 s> Date: $HTTP_DATE$\r\n
43 s> Content-Type: application/mercurial-exp-framing-0004\r\n
43 s> Content-Type: application/mercurial-exp-framing-0005\r\n
44 44 s> Transfer-Encoding: chunked\r\n
45 45 s> \r\n
46 s> 1d\r\n
47 s> \x15\x00\x00\x01\x00\x02\x012
48 s> TBk\xad\xa5\xc6u\x98\xcae\x03mW\xd9\xe4\xb6K\x0c\x1c\xe7\xa0
46 s> 28\r\n
47 s> \x00\x00\x01\x00\x02\x012
48 s> \xa1FstatusBokTBk\xad\xa5\xc6u\x98\xcae\x03mW\xd9\xe4\xb6K\x0c\x1c\xe7\xa0
49 49 s> \r\n
50 received frame(size=21; request=1; stream=2; streamflags=stream-begin; type=command-response; flags=eos)
50 received frame(size=32; request=1; stream=2; streamflags=stream-begin; type=command-response; flags=eos)
51 51 s> 0\r\n
52 52 s> \r\n
53 53 response: b'Bk\xad\xa5\xc6u\x98\xcae\x03mW\xd9\xe4\xb6K\x0c\x1c\xe7\xa0'
54 54
55 55 $ cat error.log
@@ -1,89 +1,89 b''
1 1 $ . $TESTDIR/wireprotohelpers.sh
2 2
3 3 $ hg init server
4 4 $ enablehttpv2 server
5 5 $ cd server
6 6 $ cat >> .hg/hgrc << EOF
7 7 > [web]
8 8 > push_ssl = false
9 9 > allow-push = *
10 10 > EOF
11 11 $ hg debugdrawdag << EOF
12 12 > C D
13 13 > |/
14 14 > B
15 15 > |
16 16 > A
17 17 > EOF
18 18
19 19 $ hg serve -p $HGPORT -d --pid-file hg.pid -E error.log
20 20 $ cat hg.pid > $DAEMON_PIDS
21 21
22 22 pushkey for a bookmark works
23 23
24 24 $ sendhttpv2peer << EOF
25 25 > command pushkey
26 26 > namespace bookmarks
27 27 > key @
28 28 > old
29 29 > new 426bada5c67598ca65036d57d9e4b64b0c1ce7a0
30 30 > EOF
31 31 creating http peer for wire protocol version 2
32 32 sending pushkey command
33 33 s> *\r\n (glob)
34 34 s> Accept-Encoding: identity\r\n
35 s> accept: application/mercurial-exp-framing-0004\r\n
36 s> content-type: application/mercurial-exp-framing-0004\r\n
35 s> accept: application/mercurial-exp-framing-0005\r\n
36 s> content-type: application/mercurial-exp-framing-0005\r\n
37 37 s> content-length: 105\r\n
38 38 s> host: $LOCALIP:$HGPORT\r\n (glob)
39 39 s> user-agent: Mercurial debugwireproto\r\n
40 40 s> \r\n
41 41 s> a\x00\x00\x01\x00\x01\x01\x11\xa2Dargs\xa4CkeyA@CnewX(426bada5c67598ca65036d57d9e4b64b0c1ce7a0Cold@InamespaceIbookmarksDnameGpushkey
42 42 s> makefile('rb', None)
43 43 s> HTTP/1.1 200 OK\r\n
44 44 s> Server: testing stub value\r\n
45 45 s> Date: $HTTP_DATE$\r\n
46 s> Content-Type: application/mercurial-exp-framing-0004\r\n
46 s> Content-Type: application/mercurial-exp-framing-0005\r\n
47 47 s> Transfer-Encoding: chunked\r\n
48 48 s> \r\n
49 s> 9\r\n
50 s> \x01\x00\x00\x01\x00\x02\x012
51 s> \xf5
49 s> 14\r\n
50 s> \x0c\x00\x00\x01\x00\x02\x012
51 s> \xa1FstatusBok\xf5
52 52 s> \r\n
53 received frame(size=1; request=1; stream=2; streamflags=stream-begin; type=command-response; flags=eos)
53 received frame(size=12; request=1; stream=2; streamflags=stream-begin; type=command-response; flags=eos)
54 54 s> 0\r\n
55 55 s> \r\n
56 56 response: True
57 57
58 58 $ sendhttpv2peer << EOF
59 59 > command listkeys
60 60 > namespace bookmarks
61 61 > EOF
62 62 creating http peer for wire protocol version 2
63 63 sending listkeys command
64 64 s> POST /api/exp-http-v2-0001/ro/listkeys HTTP/1.1\r\n
65 65 s> Accept-Encoding: identity\r\n
66 s> accept: application/mercurial-exp-framing-0004\r\n
67 s> content-type: application/mercurial-exp-framing-0004\r\n
66 s> accept: application/mercurial-exp-framing-0005\r\n
67 s> content-type: application/mercurial-exp-framing-0005\r\n
68 68 s> content-length: 49\r\n
69 69 s> host: $LOCALIP:$HGPORT\r\n (glob)
70 70 s> user-agent: Mercurial debugwireproto\r\n
71 71 s> \r\n
72 72 s> )\x00\x00\x01\x00\x01\x01\x11\xa2Dargs\xa1InamespaceIbookmarksDnameHlistkeys
73 73 s> makefile('rb', None)
74 74 s> HTTP/1.1 200 OK\r\n
75 75 s> Server: testing stub value\r\n
76 76 s> Date: $HTTP_DATE$\r\n
77 s> Content-Type: application/mercurial-exp-framing-0004\r\n
77 s> Content-Type: application/mercurial-exp-framing-0005\r\n
78 78 s> Transfer-Encoding: chunked\r\n
79 79 s> \r\n
80 s> 35\r\n
81 s> -\x00\x00\x01\x00\x02\x012
82 s> \xa1A@X(426bada5c67598ca65036d57d9e4b64b0c1ce7a0
80 s> 40\r\n
81 s> 8\x00\x00\x01\x00\x02\x012
82 s> \xa1FstatusBok\xa1A@X(426bada5c67598ca65036d57d9e4b64b0c1ce7a0
83 83 s> \r\n
84 received frame(size=45; request=1; stream=2; streamflags=stream-begin; type=command-response; flags=eos)
84 received frame(size=56; request=1; stream=2; streamflags=stream-begin; type=command-response; flags=eos)
85 85 s> 0\r\n
86 86 s> \r\n
87 87 response: {b'@': b'426bada5c67598ca65036d57d9e4b64b0c1ce7a0'}
88 88
89 89 $ cat error.log
@@ -1,485 +1,488 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 OK = cbor.dumps({b'status': b'ok'})
16
15 17 def makereactor(deferoutput=False):
16 18 return framing.serverreactor(deferoutput=deferoutput)
17 19
18 20 def sendframes(reactor, gen):
19 21 """Send a generator of frame bytearray to a reactor.
20 22
21 23 Emits a generator of results from ``onframerecv()`` calls.
22 24 """
23 25 for frame in gen:
24 26 header = framing.parseheader(frame)
25 27 payload = frame[framing.FRAME_HEADER_SIZE:]
26 28 assert len(payload) == header.length
27 29
28 30 yield reactor.onframerecv(framing.frame(header.requestid,
29 31 header.streamid,
30 32 header.streamflags,
31 33 header.typeid,
32 34 header.flags,
33 35 payload))
34 36
35 37 def sendcommandframes(reactor, stream, rid, cmd, args, datafh=None):
36 38 """Generate frames to run a command and send them to a reactor."""
37 39 return sendframes(reactor,
38 40 framing.createcommandframes(stream, rid, cmd, args,
39 41 datafh))
40 42
41 43
42 44 class ServerReactorTests(unittest.TestCase):
43 45 def _sendsingleframe(self, reactor, f):
44 46 results = list(sendframes(reactor, [f]))
45 47 self.assertEqual(len(results), 1)
46 48
47 49 return results[0]
48 50
49 51 def assertaction(self, res, expected):
50 52 self.assertIsInstance(res, tuple)
51 53 self.assertEqual(len(res), 2)
52 54 self.assertIsInstance(res[1], dict)
53 55 self.assertEqual(res[0], expected)
54 56
55 57 def assertframesequal(self, frames, framestrings):
56 58 expected = [ffs(s) for s in framestrings]
57 59 self.assertEqual(list(frames), expected)
58 60
59 61 def test1framecommand(self):
60 62 """Receiving a command in a single frame yields request to run it."""
61 63 reactor = makereactor()
62 64 stream = framing.stream(1)
63 65 results = list(sendcommandframes(reactor, stream, 1, b'mycommand', {}))
64 66 self.assertEqual(len(results), 1)
65 67 self.assertaction(results[0], b'runcommand')
66 68 self.assertEqual(results[0][1], {
67 69 b'requestid': 1,
68 70 b'command': b'mycommand',
69 71 b'args': {},
70 72 b'data': None,
71 73 })
72 74
73 75 result = reactor.oninputeof()
74 76 self.assertaction(result, b'noop')
75 77
76 78 def test1argument(self):
77 79 reactor = makereactor()
78 80 stream = framing.stream(1)
79 81 results = list(sendcommandframes(reactor, stream, 41, b'mycommand',
80 82 {b'foo': b'bar'}))
81 83 self.assertEqual(len(results), 1)
82 84 self.assertaction(results[0], b'runcommand')
83 85 self.assertEqual(results[0][1], {
84 86 b'requestid': 41,
85 87 b'command': b'mycommand',
86 88 b'args': {b'foo': b'bar'},
87 89 b'data': None,
88 90 })
89 91
90 92 def testmultiarguments(self):
91 93 reactor = makereactor()
92 94 stream = framing.stream(1)
93 95 results = list(sendcommandframes(reactor, stream, 1, b'mycommand',
94 96 {b'foo': b'bar', b'biz': b'baz'}))
95 97 self.assertEqual(len(results), 1)
96 98 self.assertaction(results[0], b'runcommand')
97 99 self.assertEqual(results[0][1], {
98 100 b'requestid': 1,
99 101 b'command': b'mycommand',
100 102 b'args': {b'foo': b'bar', b'biz': b'baz'},
101 103 b'data': None,
102 104 })
103 105
104 106 def testsimplecommanddata(self):
105 107 reactor = makereactor()
106 108 stream = framing.stream(1)
107 109 results = list(sendcommandframes(reactor, stream, 1, b'mycommand', {},
108 110 util.bytesio(b'data!')))
109 111 self.assertEqual(len(results), 2)
110 112 self.assertaction(results[0], b'wantframe')
111 113 self.assertaction(results[1], b'runcommand')
112 114 self.assertEqual(results[1][1], {
113 115 b'requestid': 1,
114 116 b'command': b'mycommand',
115 117 b'args': {},
116 118 b'data': b'data!',
117 119 })
118 120
119 121 def testmultipledataframes(self):
120 122 frames = [
121 123 ffs(b'1 1 stream-begin command-request new|have-data '
122 124 b"cbor:{b'name': b'mycommand'}"),
123 125 ffs(b'1 1 0 command-data continuation data1'),
124 126 ffs(b'1 1 0 command-data continuation data2'),
125 127 ffs(b'1 1 0 command-data eos data3'),
126 128 ]
127 129
128 130 reactor = makereactor()
129 131 results = list(sendframes(reactor, frames))
130 132 self.assertEqual(len(results), 4)
131 133 for i in range(3):
132 134 self.assertaction(results[i], b'wantframe')
133 135 self.assertaction(results[3], b'runcommand')
134 136 self.assertEqual(results[3][1], {
135 137 b'requestid': 1,
136 138 b'command': b'mycommand',
137 139 b'args': {},
138 140 b'data': b'data1data2data3',
139 141 })
140 142
141 143 def testargumentanddata(self):
142 144 frames = [
143 145 ffs(b'1 1 stream-begin command-request new|have-data '
144 146 b"cbor:{b'name': b'command', b'args': {b'key': b'val',"
145 147 b"b'foo': b'bar'}}"),
146 148 ffs(b'1 1 0 command-data continuation value1'),
147 149 ffs(b'1 1 0 command-data eos value2'),
148 150 ]
149 151
150 152 reactor = makereactor()
151 153 results = list(sendframes(reactor, frames))
152 154
153 155 self.assertaction(results[-1], b'runcommand')
154 156 self.assertEqual(results[-1][1], {
155 157 b'requestid': 1,
156 158 b'command': b'command',
157 159 b'args': {
158 160 b'key': b'val',
159 161 b'foo': b'bar',
160 162 },
161 163 b'data': b'value1value2',
162 164 })
163 165
164 166 def testnewandcontinuation(self):
165 167 result = self._sendsingleframe(makereactor(),
166 168 ffs(b'1 1 stream-begin command-request new|continuation '))
167 169 self.assertaction(result, b'error')
168 170 self.assertEqual(result[1], {
169 171 b'message': b'received command request frame with both new and '
170 172 b'continuation flags set',
171 173 })
172 174
173 175 def testneithernewnorcontinuation(self):
174 176 result = self._sendsingleframe(makereactor(),
175 177 ffs(b'1 1 stream-begin command-request 0 '))
176 178 self.assertaction(result, b'error')
177 179 self.assertEqual(result[1], {
178 180 b'message': b'received command request frame with neither new nor '
179 181 b'continuation flags set',
180 182 })
181 183
182 184 def testunexpectedcommanddata(self):
183 185 """Command data frame when not running a command is an error."""
184 186 result = self._sendsingleframe(makereactor(),
185 187 ffs(b'1 1 stream-begin command-data 0 ignored'))
186 188 self.assertaction(result, b'error')
187 189 self.assertEqual(result[1], {
188 190 b'message': b'expected command request frame; got 2',
189 191 })
190 192
191 193 def testunexpectedcommanddatareceiving(self):
192 194 """Same as above except the command is receiving."""
193 195 results = list(sendframes(makereactor(), [
194 196 ffs(b'1 1 stream-begin command-request new|more '
195 197 b"cbor:{b'name': b'ignored'}"),
196 198 ffs(b'1 1 0 command-data eos ignored'),
197 199 ]))
198 200
199 201 self.assertaction(results[0], b'wantframe')
200 202 self.assertaction(results[1], b'error')
201 203 self.assertEqual(results[1][1], {
202 204 b'message': b'received command data frame for request that is not '
203 205 b'expecting data: 1',
204 206 })
205 207
206 208 def testconflictingrequestidallowed(self):
207 209 """Multiple fully serviced commands with same request ID is allowed."""
208 210 reactor = makereactor()
209 211 results = []
210 212 outstream = reactor.makeoutputstream()
211 213 results.append(self._sendsingleframe(
212 214 reactor, ffs(b'1 1 stream-begin command-request new '
213 215 b"cbor:{b'name': b'command'}")))
214 216 result = reactor.oncommandresponseready(outstream, 1, b'response1')
215 217 self.assertaction(result, b'sendframes')
216 218 list(result[1][b'framegen'])
217 219 results.append(self._sendsingleframe(
218 220 reactor, ffs(b'1 1 stream-begin command-request new '
219 221 b"cbor:{b'name': b'command'}")))
220 222 result = reactor.oncommandresponseready(outstream, 1, b'response2')
221 223 self.assertaction(result, b'sendframes')
222 224 list(result[1][b'framegen'])
223 225 results.append(self._sendsingleframe(
224 226 reactor, ffs(b'1 1 stream-begin command-request new '
225 227 b"cbor:{b'name': b'command'}")))
226 228 result = reactor.oncommandresponseready(outstream, 1, b'response3')
227 229 self.assertaction(result, b'sendframes')
228 230 list(result[1][b'framegen'])
229 231
230 232 for i in range(3):
231 233 self.assertaction(results[i], b'runcommand')
232 234 self.assertEqual(results[i][1], {
233 235 b'requestid': 1,
234 236 b'command': b'command',
235 237 b'args': {},
236 238 b'data': None,
237 239 })
238 240
239 241 def testconflictingrequestid(self):
240 242 """Request ID for new command matching in-flight command is illegal."""
241 243 results = list(sendframes(makereactor(), [
242 244 ffs(b'1 1 stream-begin command-request new|more '
243 245 b"cbor:{b'name': b'command'}"),
244 246 ffs(b'1 1 0 command-request new '
245 247 b"cbor:{b'name': b'command1'}"),
246 248 ]))
247 249
248 250 self.assertaction(results[0], b'wantframe')
249 251 self.assertaction(results[1], b'error')
250 252 self.assertEqual(results[1][1], {
251 253 b'message': b'request with ID 1 already received',
252 254 })
253 255
254 256 def testinterleavedcommands(self):
255 257 cbor1 = cbor.dumps({
256 258 b'name': b'command1',
257 259 b'args': {
258 260 b'foo': b'bar',
259 261 b'key1': b'val',
260 262 }
261 263 }, canonical=True)
262 264 cbor3 = cbor.dumps({
263 265 b'name': b'command3',
264 266 b'args': {
265 267 b'biz': b'baz',
266 268 b'key': b'val',
267 269 },
268 270 }, canonical=True)
269 271
270 272 results = list(sendframes(makereactor(), [
271 273 ffs(b'1 1 stream-begin command-request new|more %s' % cbor1[0:6]),
272 274 ffs(b'3 1 0 command-request new|more %s' % cbor3[0:10]),
273 275 ffs(b'1 1 0 command-request continuation|more %s' % cbor1[6:9]),
274 276 ffs(b'3 1 0 command-request continuation|more %s' % cbor3[10:13]),
275 277 ffs(b'3 1 0 command-request continuation %s' % cbor3[13:]),
276 278 ffs(b'1 1 0 command-request continuation %s' % cbor1[9:]),
277 279 ]))
278 280
279 281 self.assertEqual([t[0] for t in results], [
280 282 b'wantframe',
281 283 b'wantframe',
282 284 b'wantframe',
283 285 b'wantframe',
284 286 b'runcommand',
285 287 b'runcommand',
286 288 ])
287 289
288 290 self.assertEqual(results[4][1], {
289 291 b'requestid': 3,
290 292 b'command': b'command3',
291 293 b'args': {b'biz': b'baz', b'key': b'val'},
292 294 b'data': None,
293 295 })
294 296 self.assertEqual(results[5][1], {
295 297 b'requestid': 1,
296 298 b'command': b'command1',
297 299 b'args': {b'foo': b'bar', b'key1': b'val'},
298 300 b'data': None,
299 301 })
300 302
301 303 def testmissingcommanddataframe(self):
302 304 # The reactor doesn't currently handle partially received commands.
303 305 # So this test is failing to do anything with request 1.
304 306 frames = [
305 307 ffs(b'1 1 stream-begin command-request new|have-data '
306 308 b"cbor:{b'name': b'command1'}"),
307 309 ffs(b'3 1 0 command-request new '
308 310 b"cbor:{b'name': b'command2'}"),
309 311 ]
310 312 results = list(sendframes(makereactor(), frames))
311 313 self.assertEqual(len(results), 2)
312 314 self.assertaction(results[0], b'wantframe')
313 315 self.assertaction(results[1], b'runcommand')
314 316
315 317 def testmissingcommanddataframeflags(self):
316 318 frames = [
317 319 ffs(b'1 1 stream-begin command-request new|have-data '
318 320 b"cbor:{b'name': b'command1'}"),
319 321 ffs(b'1 1 0 command-data 0 data'),
320 322 ]
321 323 results = list(sendframes(makereactor(), frames))
322 324 self.assertEqual(len(results), 2)
323 325 self.assertaction(results[0], b'wantframe')
324 326 self.assertaction(results[1], b'error')
325 327 self.assertEqual(results[1][1], {
326 328 b'message': b'command data frame without flags',
327 329 })
328 330
329 331 def testframefornonreceivingrequest(self):
330 332 """Receiving a frame for a command that is not receiving is illegal."""
331 333 results = list(sendframes(makereactor(), [
332 334 ffs(b'1 1 stream-begin command-request new '
333 335 b"cbor:{b'name': b'command1'}"),
334 336 ffs(b'3 1 0 command-request new|have-data '
335 337 b"cbor:{b'name': b'command3'}"),
336 338 ffs(b'5 1 0 command-data eos ignored'),
337 339 ]))
338 340 self.assertaction(results[2], b'error')
339 341 self.assertEqual(results[2][1], {
340 342 b'message': b'received frame for request that is not receiving: 5',
341 343 })
342 344
343 345 def testsimpleresponse(self):
344 346 """Bytes response to command sends result frames."""
345 347 reactor = makereactor()
346 348 instream = framing.stream(1)
347 349 list(sendcommandframes(reactor, instream, 1, b'mycommand', {}))
348 350
349 351 outstream = reactor.makeoutputstream()
350 352 result = reactor.oncommandresponseready(outstream, 1, b'response')
351 353 self.assertaction(result, b'sendframes')
352 354 self.assertframesequal(result[1][b'framegen'], [
353 b'1 2 stream-begin command-response eos response',
355 b'1 2 stream-begin command-response eos %sresponse' % OK,
354 356 ])
355 357
356 358 def testmultiframeresponse(self):
357 359 """Bytes response spanning multiple frames is handled."""
358 360 first = b'x' * framing.DEFAULT_MAX_FRAME_SIZE
359 361 second = b'y' * 100
360 362
361 363 reactor = makereactor()
362 364 instream = framing.stream(1)
363 365 list(sendcommandframes(reactor, instream, 1, b'mycommand', {}))
364 366
365 367 outstream = reactor.makeoutputstream()
366 368 result = reactor.oncommandresponseready(outstream, 1, first + second)
367 369 self.assertaction(result, b'sendframes')
368 370 self.assertframesequal(result[1][b'framegen'], [
369 b'1 2 stream-begin command-response continuation %s' % first,
371 b'1 2 stream-begin command-response continuation %s' % OK,
372 b'1 2 0 command-response continuation %s' % first,
370 373 b'1 2 0 command-response eos %s' % second,
371 374 ])
372 375
373 376 def testapplicationerror(self):
374 377 reactor = makereactor()
375 378 instream = framing.stream(1)
376 379 list(sendcommandframes(reactor, instream, 1, b'mycommand', {}))
377 380
378 381 outstream = reactor.makeoutputstream()
379 382 result = reactor.onapplicationerror(outstream, 1, b'some message')
380 383 self.assertaction(result, b'sendframes')
381 384 self.assertframesequal(result[1][b'framegen'], [
382 385 b'1 2 stream-begin error-response application some message',
383 386 ])
384 387
385 388 def test1commanddeferresponse(self):
386 389 """Responses when in deferred output mode are delayed until EOF."""
387 390 reactor = makereactor(deferoutput=True)
388 391 instream = framing.stream(1)
389 392 results = list(sendcommandframes(reactor, instream, 1, b'mycommand',
390 393 {}))
391 394 self.assertEqual(len(results), 1)
392 395 self.assertaction(results[0], b'runcommand')
393 396
394 397 outstream = reactor.makeoutputstream()
395 398 result = reactor.oncommandresponseready(outstream, 1, b'response')
396 399 self.assertaction(result, b'noop')
397 400 result = reactor.oninputeof()
398 401 self.assertaction(result, b'sendframes')
399 402 self.assertframesequal(result[1][b'framegen'], [
400 b'1 2 stream-begin command-response eos response',
403 b'1 2 stream-begin command-response eos %sresponse' % OK,
401 404 ])
402 405
403 406 def testmultiplecommanddeferresponse(self):
404 407 reactor = makereactor(deferoutput=True)
405 408 instream = framing.stream(1)
406 409 list(sendcommandframes(reactor, instream, 1, b'command1', {}))
407 410 list(sendcommandframes(reactor, instream, 3, b'command2', {}))
408 411
409 412 outstream = reactor.makeoutputstream()
410 413 result = reactor.oncommandresponseready(outstream, 1, b'response1')
411 414 self.assertaction(result, b'noop')
412 415 result = reactor.oncommandresponseready(outstream, 3, b'response2')
413 416 self.assertaction(result, b'noop')
414 417 result = reactor.oninputeof()
415 418 self.assertaction(result, b'sendframes')
416 419 self.assertframesequal(result[1][b'framegen'], [
417 b'1 2 stream-begin command-response eos response1',
418 b'3 2 0 command-response eos response2'
420 b'1 2 stream-begin command-response eos %sresponse1' % OK,
421 b'3 2 0 command-response eos %sresponse2' % OK,
419 422 ])
420 423
421 424 def testrequestidtracking(self):
422 425 reactor = makereactor(deferoutput=True)
423 426 instream = framing.stream(1)
424 427 list(sendcommandframes(reactor, instream, 1, b'command1', {}))
425 428 list(sendcommandframes(reactor, instream, 3, b'command2', {}))
426 429 list(sendcommandframes(reactor, instream, 5, b'command3', {}))
427 430
428 431 # Register results for commands out of order.
429 432 outstream = reactor.makeoutputstream()
430 433 reactor.oncommandresponseready(outstream, 3, b'response3')
431 434 reactor.oncommandresponseready(outstream, 1, b'response1')
432 435 reactor.oncommandresponseready(outstream, 5, b'response5')
433 436
434 437 result = reactor.oninputeof()
435 438 self.assertaction(result, b'sendframes')
436 439 self.assertframesequal(result[1][b'framegen'], [
437 b'3 2 stream-begin command-response eos response3',
438 b'1 2 0 command-response eos response1',
439 b'5 2 0 command-response eos response5',
440 b'3 2 stream-begin command-response eos %sresponse3' % OK,
441 b'1 2 0 command-response eos %sresponse1' % OK,
442 b'5 2 0 command-response eos %sresponse5' % OK,
440 443 ])
441 444
442 445 def testduplicaterequestonactivecommand(self):
443 446 """Receiving a request ID that matches a request that isn't finished."""
444 447 reactor = makereactor()
445 448 stream = framing.stream(1)
446 449 list(sendcommandframes(reactor, stream, 1, b'command1', {}))
447 450 results = list(sendcommandframes(reactor, stream, 1, b'command1', {}))
448 451
449 452 self.assertaction(results[0], b'error')
450 453 self.assertEqual(results[0][1], {
451 454 b'message': b'request with ID 1 is already active',
452 455 })
453 456
454 457 def testduplicaterequestonactivecommandnosend(self):
455 458 """Same as above but we've registered a response but haven't sent it."""
456 459 reactor = makereactor()
457 460 instream = framing.stream(1)
458 461 list(sendcommandframes(reactor, instream, 1, b'command1', {}))
459 462 outstream = reactor.makeoutputstream()
460 463 reactor.oncommandresponseready(outstream, 1, b'response')
461 464
462 465 # We've registered the response but haven't sent it. From the
463 466 # perspective of the reactor, the command is still active.
464 467
465 468 results = list(sendcommandframes(reactor, instream, 1, b'command1', {}))
466 469 self.assertaction(results[0], b'error')
467 470 self.assertEqual(results[0][1], {
468 471 b'message': b'request with ID 1 is already active',
469 472 })
470 473
471 474 def testduplicaterequestaftersend(self):
472 475 """We can use a duplicate request ID after we've sent the response."""
473 476 reactor = makereactor()
474 477 instream = framing.stream(1)
475 478 list(sendcommandframes(reactor, instream, 1, b'command1', {}))
476 479 outstream = reactor.makeoutputstream()
477 480 res = reactor.oncommandresponseready(outstream, 1, b'response')
478 481 list(res[1][b'framegen'])
479 482
480 483 results = list(sendcommandframes(reactor, instream, 1, b'command1', {}))
481 484 self.assertaction(results[0], b'runcommand')
482 485
483 486 if __name__ == '__main__':
484 487 import silenttestrunner
485 488 silenttestrunner.main(__name__)
@@ -1,59 +1,59 b''
1 1 HTTPV2=exp-http-v2-0001
2 MEDIATYPE=application/mercurial-exp-framing-0004
2 MEDIATYPE=application/mercurial-exp-framing-0005
3 3
4 4 sendhttpraw() {
5 5 hg --verbose debugwireproto --peer raw http://$LOCALIP:$HGPORT/
6 6 }
7 7
8 8 sendhttpv2peer() {
9 9 hg --verbose debugwireproto --nologhandshake --peer http2 http://$LOCALIP:$HGPORT/
10 10 }
11 11
12 12 sendhttpv2peerhandshake() {
13 13 hg --verbose debugwireproto --peer http2 http://$LOCALIP:$HGPORT/
14 14 }
15 15
16 16 cat > dummycommands.py << EOF
17 17 from mercurial import (
18 18 wireprototypes,
19 19 wireproto,
20 20 )
21 21
22 22 @wireproto.wireprotocommand('customreadonly', permission='pull')
23 23 def customreadonlyv1(repo, proto):
24 24 return wireprototypes.bytesresponse(b'customreadonly bytes response')
25 25
26 26 @wireproto.wireprotocommand('customreadonly', permission='pull',
27 27 transportpolicy=wireproto.POLICY_V2_ONLY)
28 28 def customreadonlyv2(repo, proto):
29 29 return wireprototypes.cborresponse(b'customreadonly bytes response')
30 30
31 31 @wireproto.wireprotocommand('customreadwrite', permission='push')
32 32 def customreadwrite(repo, proto):
33 33 return wireprototypes.bytesresponse(b'customreadwrite bytes response')
34 34
35 35 @wireproto.wireprotocommand('customreadwrite', permission='push',
36 36 transportpolicy=wireproto.POLICY_V2_ONLY)
37 37 def customreadwritev2(repo, proto):
38 38 return wireprototypes.cborresponse(b'customreadwrite bytes response')
39 39 EOF
40 40
41 41 cat >> $HGRCPATH << EOF
42 42 [extensions]
43 43 drawdag = $TESTDIR/drawdag.py
44 44 EOF
45 45
46 46 enabledummycommands() {
47 47 cat >> $HGRCPATH << EOF
48 48 [extensions]
49 49 dummycommands = $TESTTMP/dummycommands.py
50 50 EOF
51 51 }
52 52
53 53 enablehttpv2() {
54 54 cat >> $1/.hg/hgrc << EOF
55 55 [experimental]
56 56 web.apiserver = true
57 57 web.api.http-v2 = true
58 58 EOF
59 59 }
General Comments 0
You need to be logged in to leave comments. Login now