##// END OF EJS Templates
wireprotov2: implement commands as a generator of objects...
Gregory Szorc -
r39595:07b58266 default
parent child Browse files
Show More
@@ -1,310 +1,321
1 1 # error.py - Mercurial exceptions
2 2 #
3 3 # Copyright 2005-2008 Matt Mackall <mpm@selenic.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 """Mercurial exceptions.
9 9
10 10 This allows us to catch exceptions at higher levels without forcing
11 11 imports.
12 12 """
13 13
14 14 from __future__ import absolute_import
15 15
16 16 # Do not import anything but pycompat here, please
17 17 from . import pycompat
18 18
19 19 def _tobytes(exc):
20 20 """Byte-stringify exception in the same way as BaseException_str()"""
21 21 if not exc.args:
22 22 return b''
23 23 if len(exc.args) == 1:
24 24 return pycompat.bytestr(exc.args[0])
25 25 return b'(%s)' % b', '.join(b"'%s'" % pycompat.bytestr(a) for a in exc.args)
26 26
27 27 class Hint(object):
28 28 """Mix-in to provide a hint of an error
29 29
30 30 This should come first in the inheritance list to consume a hint and
31 31 pass remaining arguments to the exception class.
32 32 """
33 33 def __init__(self, *args, **kw):
34 34 self.hint = kw.pop(r'hint', None)
35 35 super(Hint, self).__init__(*args, **kw)
36 36
37 37 class RevlogError(Hint, Exception):
38 38 __bytes__ = _tobytes
39 39
40 40 class FilteredIndexError(IndexError):
41 41 __bytes__ = _tobytes
42 42
43 43 class LookupError(RevlogError, KeyError):
44 44 def __init__(self, name, index, message):
45 45 self.name = name
46 46 self.index = index
47 47 # this can't be called 'message' because at least some installs of
48 48 # Python 2.6+ complain about the 'message' property being deprecated
49 49 self.lookupmessage = message
50 50 if isinstance(name, bytes) and len(name) == 20:
51 51 from .node import short
52 52 name = short(name)
53 53 RevlogError.__init__(self, '%s@%s: %s' % (index, name, message))
54 54
55 55 def __bytes__(self):
56 56 return RevlogError.__bytes__(self)
57 57
58 58 def __str__(self):
59 59 return RevlogError.__str__(self)
60 60
61 61 class AmbiguousPrefixLookupError(LookupError):
62 62 pass
63 63
64 64 class FilteredLookupError(LookupError):
65 65 pass
66 66
67 67 class ManifestLookupError(LookupError):
68 68 pass
69 69
70 70 class CommandError(Exception):
71 71 """Exception raised on errors in parsing the command line."""
72 72 __bytes__ = _tobytes
73 73
74 74 class InterventionRequired(Hint, Exception):
75 75 """Exception raised when a command requires human intervention."""
76 76 __bytes__ = _tobytes
77 77
78 78 class Abort(Hint, Exception):
79 79 """Raised if a command needs to print an error and exit."""
80 80 __bytes__ = _tobytes
81 81
82 82 class HookLoadError(Abort):
83 83 """raised when loading a hook fails, aborting an operation
84 84
85 85 Exists to allow more specialized catching."""
86 86
87 87 class HookAbort(Abort):
88 88 """raised when a validation hook fails, aborting an operation
89 89
90 90 Exists to allow more specialized catching."""
91 91
92 92 class ConfigError(Abort):
93 93 """Exception raised when parsing config files"""
94 94
95 95 class UpdateAbort(Abort):
96 96 """Raised when an update is aborted for destination issue"""
97 97
98 98 class MergeDestAbort(Abort):
99 99 """Raised when an update is aborted for destination issues"""
100 100
101 101 class NoMergeDestAbort(MergeDestAbort):
102 102 """Raised when an update is aborted because there is nothing to merge"""
103 103
104 104 class ManyMergeDestAbort(MergeDestAbort):
105 105 """Raised when an update is aborted because destination is ambiguous"""
106 106
107 107 class ResponseExpected(Abort):
108 108 """Raised when an EOF is received for a prompt"""
109 109 def __init__(self):
110 110 from .i18n import _
111 111 Abort.__init__(self, _('response expected'))
112 112
113 113 class OutOfBandError(Hint, Exception):
114 114 """Exception raised when a remote repo reports failure"""
115 115 __bytes__ = _tobytes
116 116
117 117 class ParseError(Hint, Exception):
118 118 """Raised when parsing config files and {rev,file}sets (msg[, pos])"""
119 119 __bytes__ = _tobytes
120 120
121 121 class PatchError(Exception):
122 122 __bytes__ = _tobytes
123 123
124 124 class UnknownIdentifier(ParseError):
125 125 """Exception raised when a {rev,file}set references an unknown identifier"""
126 126
127 127 def __init__(self, function, symbols):
128 128 from .i18n import _
129 129 ParseError.__init__(self, _("unknown identifier: %s") % function)
130 130 self.function = function
131 131 self.symbols = symbols
132 132
133 133 class RepoError(Hint, Exception):
134 134 __bytes__ = _tobytes
135 135
136 136 class RepoLookupError(RepoError):
137 137 pass
138 138
139 139 class FilteredRepoLookupError(RepoLookupError):
140 140 pass
141 141
142 142 class CapabilityError(RepoError):
143 143 pass
144 144
145 145 class RequirementError(RepoError):
146 146 """Exception raised if .hg/requires has an unknown entry."""
147 147
148 148 class StdioError(IOError):
149 149 """Raised if I/O to stdout or stderr fails"""
150 150
151 151 def __init__(self, err):
152 152 IOError.__init__(self, err.errno, err.strerror)
153 153
154 154 # no __bytes__() because error message is derived from the standard IOError
155 155
156 156 class UnsupportedMergeRecords(Abort):
157 157 def __init__(self, recordtypes):
158 158 from .i18n import _
159 159 self.recordtypes = sorted(recordtypes)
160 160 s = ' '.join(self.recordtypes)
161 161 Abort.__init__(
162 162 self, _('unsupported merge state records: %s') % s,
163 163 hint=_('see https://mercurial-scm.org/wiki/MergeStateRecords for '
164 164 'more information'))
165 165
166 166 class UnknownVersion(Abort):
167 167 """generic exception for aborting from an encounter with an unknown version
168 168 """
169 169
170 170 def __init__(self, msg, hint=None, version=None):
171 171 self.version = version
172 172 super(UnknownVersion, self).__init__(msg, hint=hint)
173 173
174 174 class LockError(IOError):
175 175 def __init__(self, errno, strerror, filename, desc):
176 176 IOError.__init__(self, errno, strerror, filename)
177 177 self.desc = desc
178 178
179 179 # no __bytes__() because error message is derived from the standard IOError
180 180
181 181 class LockHeld(LockError):
182 182 def __init__(self, errno, filename, desc, locker):
183 183 LockError.__init__(self, errno, 'Lock held', filename, desc)
184 184 self.locker = locker
185 185
186 186 class LockUnavailable(LockError):
187 187 pass
188 188
189 189 # LockError is for errors while acquiring the lock -- this is unrelated
190 190 class LockInheritanceContractViolation(RuntimeError):
191 191 __bytes__ = _tobytes
192 192
193 193 class ResponseError(Exception):
194 194 """Raised to print an error with part of output and exit."""
195 195 __bytes__ = _tobytes
196 196
197 197 class UnknownCommand(Exception):
198 198 """Exception raised if command is not in the command table."""
199 199 __bytes__ = _tobytes
200 200
201 201 class AmbiguousCommand(Exception):
202 202 """Exception raised if command shortcut matches more than one command."""
203 203 __bytes__ = _tobytes
204 204
205 205 # derived from KeyboardInterrupt to simplify some breakout code
206 206 class SignalInterrupt(KeyboardInterrupt):
207 207 """Exception raised on SIGTERM and SIGHUP."""
208 208
209 209 class SignatureError(Exception):
210 210 __bytes__ = _tobytes
211 211
212 212 class PushRaced(RuntimeError):
213 213 """An exception raised during unbundling that indicate a push race"""
214 214 __bytes__ = _tobytes
215 215
216 216 class ProgrammingError(Hint, RuntimeError):
217 217 """Raised if a mercurial (core or extension) developer made a mistake"""
218 218 __bytes__ = _tobytes
219 219
220 220 class WdirUnsupported(Exception):
221 221 """An exception which is raised when 'wdir()' is not supported"""
222 222 __bytes__ = _tobytes
223 223
224 224 # bundle2 related errors
225 225 class BundleValueError(ValueError):
226 226 """error raised when bundle2 cannot be processed"""
227 227 __bytes__ = _tobytes
228 228
229 229 class BundleUnknownFeatureError(BundleValueError):
230 230 def __init__(self, parttype=None, params=(), values=()):
231 231 self.parttype = parttype
232 232 self.params = params
233 233 self.values = values
234 234 if self.parttype is None:
235 235 msg = 'Stream Parameter'
236 236 else:
237 237 msg = parttype
238 238 entries = self.params
239 239 if self.params and self.values:
240 240 assert len(self.params) == len(self.values)
241 241 entries = []
242 242 for idx, par in enumerate(self.params):
243 243 val = self.values[idx]
244 244 if val is None:
245 245 entries.append(val)
246 246 else:
247 247 entries.append("%s=%r" % (par, pycompat.maybebytestr(val)))
248 248 if entries:
249 249 msg = '%s - %s' % (msg, ', '.join(entries))
250 250 ValueError.__init__(self, msg)
251 251
252 252 class ReadOnlyPartError(RuntimeError):
253 253 """error raised when code tries to alter a part being generated"""
254 254 __bytes__ = _tobytes
255 255
256 256 class PushkeyFailed(Abort):
257 257 """error raised when a pushkey part failed to update a value"""
258 258
259 259 def __init__(self, partid, namespace=None, key=None, new=None, old=None,
260 260 ret=None):
261 261 self.partid = partid
262 262 self.namespace = namespace
263 263 self.key = key
264 264 self.new = new
265 265 self.old = old
266 266 self.ret = ret
267 267 # no i18n expected to be processed into a better message
268 268 Abort.__init__(self, 'failed to update value for "%s/%s"'
269 269 % (namespace, key))
270 270
271 271 class CensoredNodeError(RevlogError):
272 272 """error raised when content verification fails on a censored node
273 273
274 274 Also contains the tombstone data substituted for the uncensored data.
275 275 """
276 276
277 277 def __init__(self, filename, node, tombstone):
278 278 from .node import short
279 279 RevlogError.__init__(self, '%s:%s' % (filename, short(node)))
280 280 self.tombstone = tombstone
281 281
282 282 class CensoredBaseError(RevlogError):
283 283 """error raised when a delta is rejected because its base is censored
284 284
285 285 A delta based on a censored revision must be formed as single patch
286 286 operation which replaces the entire base with new content. This ensures
287 287 the delta may be applied by clones which have not censored the base.
288 288 """
289 289
290 290 class InvalidBundleSpecification(Exception):
291 291 """error raised when a bundle specification is invalid.
292 292
293 293 This is used for syntax errors as opposed to support errors.
294 294 """
295 295 __bytes__ = _tobytes
296 296
297 297 class UnsupportedBundleSpecification(Exception):
298 298 """error raised when a bundle specification is not supported."""
299 299 __bytes__ = _tobytes
300 300
301 301 class CorruptedState(Exception):
302 302 """error raised when a command is not able to read its state from file"""
303 303 __bytes__ = _tobytes
304 304
305 305 class PeerTransportError(Abort):
306 306 """Transport-level I/O error when communicating with a peer repo."""
307 307
308 308 class InMemoryMergeConflictsError(Exception):
309 309 """Exception raised when merge conflicts arose during an in-memory merge."""
310 310 __bytes__ = _tobytes
311
312 class WireprotoCommandError(Exception):
313 """Represents an error during execution of a wire protocol command.
314
315 Should only be thrown by wire protocol version 2 commands.
316
317 The error is a formatter string and an optional iterable of arguments.
318 """
319 def __init__(self, message, args=None):
320 self.message = message
321 self.messageargs = args
@@ -1,516 +1,519
1 1 **Experimental and under development**
2 2
3 3 This document describe's Mercurial's transport-agnostic remote procedure
4 4 call (RPC) protocol which is used to perform interactions with remote
5 5 servers. This protocol is also referred to as ``hgrpc``.
6 6
7 7 The protocol has the following high-level features:
8 8
9 9 * Concurrent request and response support (multiple commands can be issued
10 10 simultaneously and responses can be streamed simultaneously).
11 11 * Supports half-duplex and full-duplex connections.
12 12 * All data is transmitted within *frames*, which have a well-defined
13 13 header and encode their length.
14 14 * Side-channels for sending progress updates and printing output. Text
15 15 output from the remote can be localized locally.
16 16 * Support for simultaneous and long-lived compression streams, even across
17 17 requests.
18 18 * Uses CBOR for data exchange.
19 19
20 20 The protocol is not specific to Mercurial and could be used by other
21 21 applications.
22 22
23 23 High-level Overview
24 24 ===================
25 25
26 26 To operate the protocol, a bi-directional, half-duplex pipe supporting
27 27 ordered sends and receives is required. That is, each peer has one pipe
28 28 for sending data and another for receiving. Full-duplex pipes are also
29 29 supported.
30 30
31 31 All data is read and written in atomic units called *frames*. These
32 32 are conceptually similar to TCP packets. Higher-level functionality
33 33 is built on the exchange and processing of frames.
34 34
35 35 All frames are associated with a *stream*. A *stream* provides a
36 36 unidirectional grouping of frames. Streams facilitate two goals:
37 37 content encoding and parallelism. There is a dedicated section on
38 38 streams below.
39 39
40 40 The protocol is request-response based: the client issues requests to
41 41 the server, which issues replies to those requests. Server-initiated
42 42 messaging is not currently supported, but this specification carves
43 43 out room to implement it.
44 44
45 45 All frames are associated with a numbered request. Frames can thus
46 46 be logically grouped by their request ID.
47 47
48 48 Frames
49 49 ======
50 50
51 51 Frames begin with an 8 octet header followed by a variable length
52 52 payload::
53 53
54 54 +------------------------------------------------+
55 55 | Length (24) |
56 56 +--------------------------------+---------------+
57 57 | Request ID (16) | Stream ID (8) |
58 58 +------------------+-------------+---------------+
59 59 | Stream Flags (8) |
60 60 +-----------+------+
61 61 | Type (4) |
62 62 +-----------+
63 63 | Flags (4) |
64 64 +===========+===================================================|
65 65 | Frame Payload (0...) ...
66 66 +---------------------------------------------------------------+
67 67
68 68 The length of the frame payload is expressed as an unsigned 24 bit
69 69 little endian integer. Values larger than 65535 MUST NOT be used unless
70 70 given permission by the server as part of the negotiated capabilities
71 71 during the handshake. The frame header is not part of the advertised
72 72 frame length. The payload length is the over-the-wire length. If there
73 73 is content encoding applied to the payload as part of the frame's stream,
74 74 the length is the output of that content encoding, not the input.
75 75
76 76 The 16-bit ``Request ID`` field denotes the integer request identifier,
77 77 stored as an unsigned little endian integer. Odd numbered requests are
78 78 client-initiated. Even numbered requests are server-initiated. This
79 79 refers to where the *request* was initiated - not where the *frame* was
80 80 initiated, so servers will send frames with odd ``Request ID`` in
81 81 response to client-initiated requests. Implementations are advised to
82 82 start ordering request identifiers at ``1`` and ``0``, increment by
83 83 ``2``, and wrap around if all available numbers have been exhausted.
84 84
85 85 The 8-bit ``Stream ID`` field denotes the stream that the frame is
86 86 associated with. Frames belonging to a stream may have content
87 87 encoding applied and the receiver may need to decode the raw frame
88 88 payload to obtain the original data. Odd numbered IDs are
89 89 client-initiated. Even numbered IDs are server-initiated.
90 90
91 91 The 8-bit ``Stream Flags`` field defines stream processing semantics.
92 92 See the section on streams below.
93 93
94 94 The 4-bit ``Type`` field denotes the type of frame being sent.
95 95
96 96 The 4-bit ``Flags`` field defines special, per-type attributes for
97 97 the frame.
98 98
99 99 The sections below define the frame types and their behavior.
100 100
101 101 Command Request (``0x01``)
102 102 --------------------------
103 103
104 104 This frame contains a request to run a command.
105 105
106 106 The payload consists of a CBOR map defining the command request. The
107 107 bytestring keys of that map are:
108 108
109 109 name
110 110 Name of the command that should be executed (bytestring).
111 111 args
112 112 Map of bytestring keys to various value types containing the named
113 113 arguments to this command.
114 114
115 115 Each command defines its own set of argument names and their expected
116 116 types.
117 117
118 118 This frame type MUST ONLY be sent from clients to servers: it is illegal
119 119 for a server to send this frame to a client.
120 120
121 121 The following flag values are defined for this type:
122 122
123 123 0x01
124 124 New command request. When set, this frame represents the beginning
125 125 of a new request to run a command. The ``Request ID`` attached to this
126 126 frame MUST NOT be active.
127 127 0x02
128 128 Command request continuation. When set, this frame is a continuation
129 129 from a previous command request frame for its ``Request ID``. This
130 130 flag is set when the CBOR data for a command request does not fit
131 131 in a single frame.
132 132 0x04
133 133 Additional frames expected. When set, the command request didn't fit
134 134 into a single frame and additional CBOR data follows in a subsequent
135 135 frame.
136 136 0x08
137 137 Command data frames expected. When set, command data frames are
138 138 expected to follow the final command request frame for this request.
139 139
140 140 ``0x01`` MUST be set on the initial command request frame for a
141 141 ``Request ID``.
142 142
143 143 ``0x01`` or ``0x02`` MUST be set to indicate this frame's role in
144 144 a series of command request frames.
145 145
146 146 If command data frames are to be sent, ``0x08`` MUST be set on ALL
147 147 command request frames.
148 148
149 149 Command Data (``0x02``)
150 150 -----------------------
151 151
152 152 This frame contains raw data for a command.
153 153
154 154 Most commands can be executed by specifying arguments. However,
155 155 arguments have an upper bound to their length. For commands that
156 156 accept data that is beyond this length or whose length isn't known
157 157 when the command is initially sent, they will need to stream
158 158 arbitrary data to the server. This frame type facilitates the sending
159 159 of this data.
160 160
161 161 The payload of this frame type consists of a stream of raw data to be
162 162 consumed by the command handler on the server. The format of the data
163 163 is command specific.
164 164
165 165 The following flag values are defined for this type:
166 166
167 167 0x01
168 168 Command data continuation. When set, the data for this command
169 169 continues into a subsequent frame.
170 170
171 171 0x02
172 172 End of data. When set, command data has been fully sent to the
173 173 server. The command has been fully issued and no new data for this
174 174 command will be sent. The next frame will belong to a new command.
175 175
176 176 Command Response Data (``0x03``)
177 177 --------------------------------
178 178
179 179 This frame contains response data to an issued command.
180 180
181 181 Response data ALWAYS consists of a series of 1 or more CBOR encoded
182 182 values. A CBOR value may be using indefinite length encoding. And the
183 183 bytes constituting the value may span several frames.
184 184
185 185 The following flag values are defined for this type:
186 186
187 187 0x01
188 188 Data continuation. When set, an additional frame containing response data
189 189 will follow.
190 190 0x02
191 191 End of data. When set, the response data has been fully sent and
192 192 no additional frames for this response will be sent.
193 193
194 194 The ``0x01`` flag is mutually exclusive with the ``0x02`` flag.
195 195
196 196 Error Occurred (``0x05``)
197 197 -------------------------
198 198
199 199 Some kind of error occurred.
200 200
201 201 There are 3 general kinds of failures that can occur:
202 202
203 203 * Command error encountered before any response issued
204 204 * Command error encountered after a response was issued
205 205 * Protocol or stream level error
206 206
207 207 This frame type is used to capture the latter cases. (The general
208 208 command error case is handled by the leading CBOR map in
209 209 ``Command Response`` frames.)
210 210
211 211 The payload of this frame contains a CBOR map detailing the error. That
212 212 map has the following bytestring keys:
213 213
214 214 type
215 215 (bytestring) The overall type of error encountered. Can be one of the
216 216 following values:
217 217
218 218 protocol
219 219 A protocol-level error occurred. This typically means someone
220 220 is violating the framing protocol semantics and the server is
221 221 refusing to proceed.
222 222
223 223 server
224 224 A server-level error occurred. This typically indicates some kind of
225 225 logic error on the server, likely the fault of the server.
226 226
227 227 command
228 228 A command-level error, likely the fault of the client.
229 229
230 230 message
231 231 (array of maps) A richly formatted message that is intended for
232 232 human consumption. See the ``Human Output Side-Channel`` frame
233 233 section for a description of the format of this data structure.
234 234
235 235 Human Output Side-Channel (``0x06``)
236 236 ------------------------------------
237 237
238 238 This frame contains a message that is intended to be displayed to
239 239 people. Whereas most frames communicate machine readable data, this
240 240 frame communicates textual data that is intended to be shown to
241 241 humans.
242 242
243 243 The frame consists of a series of *formatting requests*. Each formatting
244 244 request consists of a formatting string, arguments for that formatting
245 245 string, and labels to apply to that formatting string.
246 246
247 247 A formatting string is a printf()-like string that allows variable
248 248 substitution within the string. Labels allow the rendered text to be
249 249 *decorated*. Assuming use of the canonical Mercurial code base, a
250 250 formatting string can be the input to the ``i18n._`` function. This
251 251 allows messages emitted from the server to be localized. So even if
252 252 the server has different i18n settings, people could see messages in
253 253 their *native* settings. Similarly, the use of labels allows
254 254 decorations like coloring and underlining to be applied using the
255 255 client's configured rendering settings.
256 256
257 257 Formatting strings are similar to ``printf()`` strings or how
258 258 Python's ``%`` operator works. The only supported formatting sequences
259 259 are ``%s`` and ``%%``. ``%s`` will be replaced by whatever the string
260 260 at that position resolves to. ``%%`` will be replaced by ``%``. All
261 261 other 2-byte sequences beginning with ``%`` represent a literal
262 262 ``%`` followed by that character. However, future versions of the
263 263 wire protocol reserve the right to allow clients to opt in to receiving
264 264 formatting strings with additional formatters, hence why ``%%`` is
265 265 required to represent the literal ``%``.
266 266
267 267 The frame payload consists of a CBOR array of CBOR maps. Each map
268 268 defines an *atom* of text data to print. Each *atom* has the following
269 269 bytestring keys:
270 270
271 271 msg
272 272 (bytestring) The formatting string. Content MUST be ASCII.
273 273 args (optional)
274 274 Array of bytestrings defining arguments to the formatting string.
275 275 labels (optional)
276 276 Array of bytestrings defining labels to apply to this atom.
277 277
278 278 All data to be printed MUST be encoded into a single frame: this frame
279 279 does not support spanning data across multiple frames.
280 280
281 281 All textual data encoded in these frames is assumed to be line delimited.
282 282 The last atom in the frame SHOULD end with a newline (``\n``). If it
283 283 doesn't, clients MAY add a newline to facilitate immediate printing.
284 284
285 285 Progress Update (``0x07``)
286 286 --------------------------
287 287
288 288 This frame holds the progress of an operation on the peer. Consumption
289 289 of these frames allows clients to display progress bars, estimated
290 290 completion times, etc.
291 291
292 292 Each frame defines the progress of a single operation on the peer. The
293 293 payload consists of a CBOR map with the following bytestring keys:
294 294
295 295 topic
296 296 Topic name (string)
297 297 pos
298 298 Current numeric position within the topic (integer)
299 299 total
300 300 Total/end numeric position of this topic (unsigned integer)
301 301 label (optional)
302 302 Unit label (string)
303 303 item (optional)
304 304 Item name (string)
305 305
306 306 Progress state is created when a frame is received referencing a
307 307 *topic* that isn't currently tracked. Progress tracking for that
308 308 *topic* is finished when a frame is received reporting the current
309 309 position of that topic as ``-1``.
310 310
311 311 Multiple *topics* may be active at any given time.
312 312
313 313 Rendering of progress information is not mandated or governed by this
314 314 specification: implementations MAY render progress information however
315 315 they see fit, including not at all.
316 316
317 317 The string data describing the topic SHOULD be static strings to
318 318 facilitate receivers localizing that string data. The emitter
319 319 MUST normalize all string data to valid UTF-8 and receivers SHOULD
320 320 validate that received data conforms to UTF-8. The topic name
321 321 SHOULD be ASCII.
322 322
323 323 Stream Encoding Settings (``0x08``)
324 324 -----------------------------------
325 325
326 326 This frame type holds information defining the content encoding
327 327 settings for a *stream*.
328 328
329 329 This frame type is likely consumed by the protocol layer and is not
330 330 passed on to applications.
331 331
332 332 This frame type MUST ONLY occur on frames having the *Beginning of Stream*
333 333 ``Stream Flag`` set.
334 334
335 335 The payload of this frame defines what content encoding has (possibly)
336 336 been applied to the payloads of subsequent frames in this stream.
337 337
338 338 The payload begins with an 8-bit integer defining the length of the
339 339 encoding *profile*, followed by the string name of that profile, which
340 340 must be an ASCII string. All bytes that follow can be used by that
341 341 profile for supplemental settings definitions. See the section below
342 342 on defined encoding profiles.
343 343
344 344 Stream States and Flags
345 345 =======================
346 346
347 347 Streams can be in two states: *open* and *closed*. An *open* stream
348 348 is active and frames attached to that stream could arrive at any time.
349 349 A *closed* stream is not active. If a frame attached to a *closed*
350 350 stream arrives, that frame MUST have an appropriate stream flag
351 351 set indicating beginning of stream. All streams are in the *closed*
352 352 state by default.
353 353
354 354 The ``Stream Flags`` field denotes a set of bit flags for defining
355 355 the relationship of this frame within a stream. The following flags
356 356 are defined:
357 357
358 358 0x01
359 359 Beginning of stream. The first frame in the stream MUST set this
360 360 flag. When received, the ``Stream ID`` this frame is attached to
361 361 becomes ``open``.
362 362
363 363 0x02
364 364 End of stream. The last frame in a stream MUST set this flag. When
365 365 received, the ``Stream ID`` this frame is attached to becomes
366 366 ``closed``. Any content encoding context associated with this stream
367 367 can be destroyed after processing the payload of this frame.
368 368
369 369 0x04
370 370 Apply content encoding. When set, any content encoding settings
371 371 defined by the stream should be applied when attempting to read
372 372 the frame. When not set, the frame payload isn't encoded.
373 373
374 374 Streams
375 375 =======
376 376
377 377 Streams - along with ``Request IDs`` - facilitate grouping of frames.
378 378 But the purpose of each is quite different and the groupings they
379 379 constitute are independent.
380 380
381 381 A ``Request ID`` is essentially a tag. It tells you which logical
382 382 request a frame is associated with.
383 383
384 384 A *stream* is a sequence of frames grouped for the express purpose
385 385 of applying a stateful encoding or for denoting sub-groups of frames.
386 386
387 387 Unlike ``Request ID``s which span the request and response, a stream
388 388 is unidirectional and stream IDs are independent from client to
389 389 server.
390 390
391 391 There is no strict hierarchical relationship between ``Request IDs``
392 392 and *streams*. A stream can contain frames having multiple
393 393 ``Request IDs``. Frames belonging to the same ``Request ID`` can
394 394 span multiple streams.
395 395
396 396 One goal of streams is to facilitate content encoding. A stream can
397 397 define an encoding to be applied to frame payloads. For example, the
398 398 payload transmitted over the wire may contain output from a
399 399 zstandard compression operation and the receiving end may decompress
400 400 that payload to obtain the original data.
401 401
402 402 The other goal of streams is to facilitate concurrent execution. For
403 403 example, a server could spawn 4 threads to service a request that can
404 404 be easily parallelized. Each of those 4 threads could write into its
405 405 own stream. Those streams could then in turn be delivered to 4 threads
406 406 on the receiving end, with each thread consuming its stream in near
407 407 isolation. The *main* thread on both ends merely does I/O and
408 408 encodes/decodes frame headers: the bulk of the work is done by worker
409 409 threads.
410 410
411 411 In addition, since content encoding is defined per stream, each
412 412 *worker thread* could perform potentially CPU bound work concurrently
413 413 with other threads. This approach of applying encoding at the
414 414 sub-protocol / stream level eliminates a potential resource constraint
415 415 on the protocol stream as a whole (it is common for the throughput of
416 416 a compression engine to be smaller than the throughput of a network).
417 417
418 418 Having multiple streams - each with their own encoding settings - also
419 419 facilitates the use of advanced data compression techniques. For
420 420 example, a transmitter could see that it is generating data faster
421 421 and slower than the receiving end is consuming it and adjust its
422 422 compression settings to trade CPU for compression ratio accordingly.
423 423
424 424 While streams can define a content encoding, not all frames within
425 425 that stream must use that content encoding. This can be useful when
426 426 data is being served from caches and being derived dynamically. A
427 427 cache could pre-compressed data so the server doesn't have to
428 428 recompress it. The ability to pick and choose which frames are
429 429 compressed allows servers to easily send data to the wire without
430 430 involving potentially expensive encoding overhead.
431 431
432 432 Content Encoding Profiles
433 433 =========================
434 434
435 435 Streams can have named content encoding *profiles* associated with
436 436 them. A profile defines a shared understanding of content encoding
437 437 settings and behavior.
438 438
439 439 The following profiles are defined:
440 440
441 441 TBD
442 442
443 443 Command Protocol
444 444 ================
445 445
446 446 A client can request that a remote run a command by sending it
447 447 frames defining that command. This logical stream is composed of
448 448 1 or more ``Command Request`` frames and and 0 or more ``Command Data``
449 449 frames.
450 450
451 451 All frames composing a single command request MUST be associated with
452 452 the same ``Request ID``.
453 453
454 454 Clients MAY send additional command requests without waiting on the
455 455 response to a previous command request. If they do so, they MUST ensure
456 456 that the ``Request ID`` field of outbound frames does not conflict
457 457 with that of an active ``Request ID`` whose response has not yet been
458 458 fully received.
459 459
460 460 Servers MAY respond to commands in a different order than they were
461 461 sent over the wire. Clients MUST be prepared to deal with this. Servers
462 462 also MAY start executing commands in a different order than they were
463 463 received, or MAY execute multiple commands concurrently.
464 464
465 465 If there is a dependency between commands or a race condition between
466 466 commands executing (e.g. a read-only command that depends on the results
467 467 of a command that mutates the repository), then clients MUST NOT send
468 468 frames issuing a command until a response to all dependent commands has
469 469 been received.
470 470 TODO think about whether we should express dependencies between commands
471 471 to avoid roundtrip latency.
472 472
473 473 A command is defined by a command name, 0 or more command arguments,
474 474 and optional command data.
475 475
476 476 Arguments are the recommended mechanism for transferring fixed sets of
477 477 parameters to a command. Data is appropriate for transferring variable
478 478 data. Thinking in terms of HTTP, arguments would be headers and data
479 479 would be the message body.
480 480
481 481 It is recommended for servers to delay the dispatch of a command
482 482 until all argument have been received. Servers MAY impose limits on the
483 483 maximum argument size.
484 484 TODO define failure mechanism.
485 485
486 486 Servers MAY dispatch to commands immediately once argument data
487 487 is available or delay until command data is received in full.
488 488
489 489 Once a ``Command Request`` frame is sent, a client must be prepared to
490 490 receive any of the following frames associated with that request:
491 491 ``Command Response``, ``Error Response``, ``Human Output Side-Channel``,
492 492 ``Progress Update``.
493 493
494 494 The *main* response for a command will be in ``Command Response`` frames.
495 495 The payloads of these frames consist of 1 or more CBOR encoded values.
496 496 The first CBOR value on the first ``Command Response`` frame is special
497 497 and denotes the overall status of the command. This CBOR map contains
498 498 the following bytestring keys:
499 499
500 500 status
501 501 (bytestring) A well-defined message containing the overall status of
502 502 this command request. The following values are defined:
503 503
504 504 ok
505 505 The command was received successfully and its response follows.
506 506 error
507 507 There was an error processing the command. More details about the
508 508 error are encoded in the ``error`` key.
509 509
510 510 error (optional)
511 511 A map containing information about an encountered error. The map has the
512 512 following keys:
513 513
514 514 message
515 515 (array of maps) A message describing the error. The message uses the
516 516 same format as those in the ``Human Output Side-Channel`` frame.
517
518 TODO formalize when error frames can be seen and how errors can be
519 recognized midway through a command response.
@@ -1,1169 +1,1230
1 1 # wireprotoframing.py - unified framing protocol for wire protocol
2 2 #
3 3 # Copyright 2018 Gregory Szorc <gregory.szorc@gmail.com>
4 4 #
5 5 # This software may be used and distributed according to the terms of the
6 6 # GNU General Public License version 2 or any later version.
7 7
8 8 # This file contains functionality to support the unified frame-based wire
9 9 # protocol. For details about the protocol, see
10 10 # `hg help internals.wireprotocol`.
11 11
12 12 from __future__ import absolute_import
13 13
14 14 import collections
15 15 import struct
16 16
17 17 from .i18n import _
18 18 from .thirdparty import (
19 19 attr,
20 20 )
21 21 from . import (
22 22 encoding,
23 23 error,
24 24 util,
25 25 )
26 26 from .utils import (
27 27 cborutil,
28 28 stringutil,
29 29 )
30 30
31 31 FRAME_HEADER_SIZE = 8
32 32 DEFAULT_MAX_FRAME_SIZE = 32768
33 33
34 34 STREAM_FLAG_BEGIN_STREAM = 0x01
35 35 STREAM_FLAG_END_STREAM = 0x02
36 36 STREAM_FLAG_ENCODING_APPLIED = 0x04
37 37
38 38 STREAM_FLAGS = {
39 39 b'stream-begin': STREAM_FLAG_BEGIN_STREAM,
40 40 b'stream-end': STREAM_FLAG_END_STREAM,
41 41 b'encoded': STREAM_FLAG_ENCODING_APPLIED,
42 42 }
43 43
44 44 FRAME_TYPE_COMMAND_REQUEST = 0x01
45 45 FRAME_TYPE_COMMAND_DATA = 0x02
46 46 FRAME_TYPE_COMMAND_RESPONSE = 0x03
47 47 FRAME_TYPE_ERROR_RESPONSE = 0x05
48 48 FRAME_TYPE_TEXT_OUTPUT = 0x06
49 49 FRAME_TYPE_PROGRESS = 0x07
50 50 FRAME_TYPE_STREAM_SETTINGS = 0x08
51 51
52 52 FRAME_TYPES = {
53 53 b'command-request': FRAME_TYPE_COMMAND_REQUEST,
54 54 b'command-data': FRAME_TYPE_COMMAND_DATA,
55 55 b'command-response': FRAME_TYPE_COMMAND_RESPONSE,
56 56 b'error-response': FRAME_TYPE_ERROR_RESPONSE,
57 57 b'text-output': FRAME_TYPE_TEXT_OUTPUT,
58 58 b'progress': FRAME_TYPE_PROGRESS,
59 59 b'stream-settings': FRAME_TYPE_STREAM_SETTINGS,
60 60 }
61 61
62 62 FLAG_COMMAND_REQUEST_NEW = 0x01
63 63 FLAG_COMMAND_REQUEST_CONTINUATION = 0x02
64 64 FLAG_COMMAND_REQUEST_MORE_FRAMES = 0x04
65 65 FLAG_COMMAND_REQUEST_EXPECT_DATA = 0x08
66 66
67 67 FLAGS_COMMAND_REQUEST = {
68 68 b'new': FLAG_COMMAND_REQUEST_NEW,
69 69 b'continuation': FLAG_COMMAND_REQUEST_CONTINUATION,
70 70 b'more': FLAG_COMMAND_REQUEST_MORE_FRAMES,
71 71 b'have-data': FLAG_COMMAND_REQUEST_EXPECT_DATA,
72 72 }
73 73
74 74 FLAG_COMMAND_DATA_CONTINUATION = 0x01
75 75 FLAG_COMMAND_DATA_EOS = 0x02
76 76
77 77 FLAGS_COMMAND_DATA = {
78 78 b'continuation': FLAG_COMMAND_DATA_CONTINUATION,
79 79 b'eos': FLAG_COMMAND_DATA_EOS,
80 80 }
81 81
82 82 FLAG_COMMAND_RESPONSE_CONTINUATION = 0x01
83 83 FLAG_COMMAND_RESPONSE_EOS = 0x02
84 84
85 85 FLAGS_COMMAND_RESPONSE = {
86 86 b'continuation': FLAG_COMMAND_RESPONSE_CONTINUATION,
87 87 b'eos': FLAG_COMMAND_RESPONSE_EOS,
88 88 }
89 89
90 90 # Maps frame types to their available flags.
91 91 FRAME_TYPE_FLAGS = {
92 92 FRAME_TYPE_COMMAND_REQUEST: FLAGS_COMMAND_REQUEST,
93 93 FRAME_TYPE_COMMAND_DATA: FLAGS_COMMAND_DATA,
94 94 FRAME_TYPE_COMMAND_RESPONSE: FLAGS_COMMAND_RESPONSE,
95 95 FRAME_TYPE_ERROR_RESPONSE: {},
96 96 FRAME_TYPE_TEXT_OUTPUT: {},
97 97 FRAME_TYPE_PROGRESS: {},
98 98 FRAME_TYPE_STREAM_SETTINGS: {},
99 99 }
100 100
101 101 ARGUMENT_RECORD_HEADER = struct.Struct(r'<HH')
102 102
103 103 def humanflags(mapping, value):
104 104 """Convert a numeric flags value to a human value, using a mapping table."""
105 105 namemap = {v: k for k, v in mapping.iteritems()}
106 106 flags = []
107 107 val = 1
108 108 while value >= val:
109 109 if value & val:
110 110 flags.append(namemap.get(val, '<unknown 0x%02x>' % val))
111 111 val <<= 1
112 112
113 113 return b'|'.join(flags)
114 114
115 115 @attr.s(slots=True)
116 116 class frameheader(object):
117 117 """Represents the data in a frame header."""
118 118
119 119 length = attr.ib()
120 120 requestid = attr.ib()
121 121 streamid = attr.ib()
122 122 streamflags = attr.ib()
123 123 typeid = attr.ib()
124 124 flags = attr.ib()
125 125
126 126 @attr.s(slots=True, repr=False)
127 127 class frame(object):
128 128 """Represents a parsed frame."""
129 129
130 130 requestid = attr.ib()
131 131 streamid = attr.ib()
132 132 streamflags = attr.ib()
133 133 typeid = attr.ib()
134 134 flags = attr.ib()
135 135 payload = attr.ib()
136 136
137 137 @encoding.strmethod
138 138 def __repr__(self):
139 139 typename = '<unknown 0x%02x>' % self.typeid
140 140 for name, value in FRAME_TYPES.iteritems():
141 141 if value == self.typeid:
142 142 typename = name
143 143 break
144 144
145 145 return ('frame(size=%d; request=%d; stream=%d; streamflags=%s; '
146 146 'type=%s; flags=%s)' % (
147 147 len(self.payload), self.requestid, self.streamid,
148 148 humanflags(STREAM_FLAGS, self.streamflags), typename,
149 149 humanflags(FRAME_TYPE_FLAGS.get(self.typeid, {}), self.flags)))
150 150
151 151 def makeframe(requestid, streamid, streamflags, typeid, flags, payload):
152 152 """Assemble a frame into a byte array."""
153 153 # TODO assert size of payload.
154 154 frame = bytearray(FRAME_HEADER_SIZE + len(payload))
155 155
156 156 # 24 bits length
157 157 # 16 bits request id
158 158 # 8 bits stream id
159 159 # 8 bits stream flags
160 160 # 4 bits type
161 161 # 4 bits flags
162 162
163 163 l = struct.pack(r'<I', len(payload))
164 164 frame[0:3] = l[0:3]
165 165 struct.pack_into(r'<HBB', frame, 3, requestid, streamid, streamflags)
166 166 frame[7] = (typeid << 4) | flags
167 167 frame[8:] = payload
168 168
169 169 return frame
170 170
171 171 def makeframefromhumanstring(s):
172 172 """Create a frame from a human readable string
173 173
174 174 Strings have the form:
175 175
176 176 <request-id> <stream-id> <stream-flags> <type> <flags> <payload>
177 177
178 178 This can be used by user-facing applications and tests for creating
179 179 frames easily without having to type out a bunch of constants.
180 180
181 181 Request ID and stream IDs are integers.
182 182
183 183 Stream flags, frame type, and flags can be specified by integer or
184 184 named constant.
185 185
186 186 Flags can be delimited by `|` to bitwise OR them together.
187 187
188 188 If the payload begins with ``cbor:``, the following string will be
189 189 evaluated as Python literal and the resulting object will be fed into
190 190 a CBOR encoder. Otherwise, the payload is interpreted as a Python
191 191 byte string literal.
192 192 """
193 193 fields = s.split(b' ', 5)
194 194 requestid, streamid, streamflags, frametype, frameflags, payload = fields
195 195
196 196 requestid = int(requestid)
197 197 streamid = int(streamid)
198 198
199 199 finalstreamflags = 0
200 200 for flag in streamflags.split(b'|'):
201 201 if flag in STREAM_FLAGS:
202 202 finalstreamflags |= STREAM_FLAGS[flag]
203 203 else:
204 204 finalstreamflags |= int(flag)
205 205
206 206 if frametype in FRAME_TYPES:
207 207 frametype = FRAME_TYPES[frametype]
208 208 else:
209 209 frametype = int(frametype)
210 210
211 211 finalflags = 0
212 212 validflags = FRAME_TYPE_FLAGS[frametype]
213 213 for flag in frameflags.split(b'|'):
214 214 if flag in validflags:
215 215 finalflags |= validflags[flag]
216 216 else:
217 217 finalflags |= int(flag)
218 218
219 219 if payload.startswith(b'cbor:'):
220 220 payload = b''.join(cborutil.streamencode(
221 221 stringutil.evalpythonliteral(payload[5:])))
222 222
223 223 else:
224 224 payload = stringutil.unescapestr(payload)
225 225
226 226 return makeframe(requestid=requestid, streamid=streamid,
227 227 streamflags=finalstreamflags, typeid=frametype,
228 228 flags=finalflags, payload=payload)
229 229
230 230 def parseheader(data):
231 231 """Parse a unified framing protocol frame header from a buffer.
232 232
233 233 The header is expected to be in the buffer at offset 0 and the
234 234 buffer is expected to be large enough to hold a full header.
235 235 """
236 236 # 24 bits payload length (little endian)
237 237 # 16 bits request ID
238 238 # 8 bits stream ID
239 239 # 8 bits stream flags
240 240 # 4 bits frame type
241 241 # 4 bits frame flags
242 242 # ... payload
243 243 framelength = data[0] + 256 * data[1] + 16384 * data[2]
244 244 requestid, streamid, streamflags = struct.unpack_from(r'<HBB', data, 3)
245 245 typeflags = data[7]
246 246
247 247 frametype = (typeflags & 0xf0) >> 4
248 248 frameflags = typeflags & 0x0f
249 249
250 250 return frameheader(framelength, requestid, streamid, streamflags,
251 251 frametype, frameflags)
252 252
253 253 def readframe(fh):
254 254 """Read a unified framing protocol frame from a file object.
255 255
256 256 Returns a 3-tuple of (type, flags, payload) for the decoded frame or
257 257 None if no frame is available. May raise if a malformed frame is
258 258 seen.
259 259 """
260 260 header = bytearray(FRAME_HEADER_SIZE)
261 261
262 262 readcount = fh.readinto(header)
263 263
264 264 if readcount == 0:
265 265 return None
266 266
267 267 if readcount != FRAME_HEADER_SIZE:
268 268 raise error.Abort(_('received incomplete frame: got %d bytes: %s') %
269 269 (readcount, header))
270 270
271 271 h = parseheader(header)
272 272
273 273 payload = fh.read(h.length)
274 274 if len(payload) != h.length:
275 275 raise error.Abort(_('frame length error: expected %d; got %d') %
276 276 (h.length, len(payload)))
277 277
278 278 return frame(h.requestid, h.streamid, h.streamflags, h.typeid, h.flags,
279 279 payload)
280 280
281 281 def createcommandframes(stream, requestid, cmd, args, datafh=None,
282 282 maxframesize=DEFAULT_MAX_FRAME_SIZE):
283 283 """Create frames necessary to transmit a request to run a command.
284 284
285 285 This is a generator of bytearrays. Each item represents a frame
286 286 ready to be sent over the wire to a peer.
287 287 """
288 288 data = {b'name': cmd}
289 289 if args:
290 290 data[b'args'] = args
291 291
292 292 data = b''.join(cborutil.streamencode(data))
293 293
294 294 offset = 0
295 295
296 296 while True:
297 297 flags = 0
298 298
299 299 # Must set new or continuation flag.
300 300 if not offset:
301 301 flags |= FLAG_COMMAND_REQUEST_NEW
302 302 else:
303 303 flags |= FLAG_COMMAND_REQUEST_CONTINUATION
304 304
305 305 # Data frames is set on all frames.
306 306 if datafh:
307 307 flags |= FLAG_COMMAND_REQUEST_EXPECT_DATA
308 308
309 309 payload = data[offset:offset + maxframesize]
310 310 offset += len(payload)
311 311
312 312 if len(payload) == maxframesize and offset < len(data):
313 313 flags |= FLAG_COMMAND_REQUEST_MORE_FRAMES
314 314
315 315 yield stream.makeframe(requestid=requestid,
316 316 typeid=FRAME_TYPE_COMMAND_REQUEST,
317 317 flags=flags,
318 318 payload=payload)
319 319
320 320 if not (flags & FLAG_COMMAND_REQUEST_MORE_FRAMES):
321 321 break
322 322
323 323 if datafh:
324 324 while True:
325 325 data = datafh.read(DEFAULT_MAX_FRAME_SIZE)
326 326
327 327 done = False
328 328 if len(data) == DEFAULT_MAX_FRAME_SIZE:
329 329 flags = FLAG_COMMAND_DATA_CONTINUATION
330 330 else:
331 331 flags = FLAG_COMMAND_DATA_EOS
332 332 assert datafh.read(1) == b''
333 333 done = True
334 334
335 335 yield stream.makeframe(requestid=requestid,
336 336 typeid=FRAME_TYPE_COMMAND_DATA,
337 337 flags=flags,
338 338 payload=data)
339 339
340 340 if done:
341 341 break
342 342
343 343 def createcommandresponseframesfrombytes(stream, requestid, data,
344 344 maxframesize=DEFAULT_MAX_FRAME_SIZE):
345 345 """Create a raw frame to send a bytes response from static bytes input.
346 346
347 347 Returns a generator of bytearrays.
348 348 """
349 349 # Automatically send the overall CBOR response map.
350 350 overall = b''.join(cborutil.streamencode({b'status': b'ok'}))
351 351 if len(overall) > maxframesize:
352 352 raise error.ProgrammingError('not yet implemented')
353 353
354 354 # Simple case where we can fit the full response in a single frame.
355 355 if len(overall) + len(data) <= maxframesize:
356 356 flags = FLAG_COMMAND_RESPONSE_EOS
357 357 yield stream.makeframe(requestid=requestid,
358 358 typeid=FRAME_TYPE_COMMAND_RESPONSE,
359 359 flags=flags,
360 360 payload=overall + data)
361 361 return
362 362
363 363 # It's easier to send the overall CBOR map in its own frame than to track
364 364 # offsets.
365 365 yield stream.makeframe(requestid=requestid,
366 366 typeid=FRAME_TYPE_COMMAND_RESPONSE,
367 367 flags=FLAG_COMMAND_RESPONSE_CONTINUATION,
368 368 payload=overall)
369 369
370 370 offset = 0
371 371 while True:
372 372 chunk = data[offset:offset + maxframesize]
373 373 offset += len(chunk)
374 374 done = offset == len(data)
375 375
376 376 if done:
377 377 flags = FLAG_COMMAND_RESPONSE_EOS
378 378 else:
379 379 flags = FLAG_COMMAND_RESPONSE_CONTINUATION
380 380
381 381 yield stream.makeframe(requestid=requestid,
382 382 typeid=FRAME_TYPE_COMMAND_RESPONSE,
383 383 flags=flags,
384 384 payload=chunk)
385 385
386 386 if done:
387 387 break
388 388
389 389 def createbytesresponseframesfromgen(stream, requestid, gen,
390 390 maxframesize=DEFAULT_MAX_FRAME_SIZE):
391 overall = b''.join(cborutil.streamencode({b'status': b'ok'}))
391 """Generator of frames from a generator of byte chunks.
392 392
393 yield stream.makeframe(requestid=requestid,
394 typeid=FRAME_TYPE_COMMAND_RESPONSE,
395 flags=FLAG_COMMAND_RESPONSE_CONTINUATION,
396 payload=overall)
397
393 This assumes that another frame will follow whatever this emits. i.e.
394 this always emits the continuation flag and never emits the end-of-stream
395 flag.
396 """
398 397 cb = util.chunkbuffer(gen)
399
400 flags = 0
398 flags = FLAG_COMMAND_RESPONSE_CONTINUATION
401 399
402 400 while True:
403 401 chunk = cb.read(maxframesize)
404 402 if not chunk:
405 403 break
406 404
407 405 yield stream.makeframe(requestid=requestid,
408 406 typeid=FRAME_TYPE_COMMAND_RESPONSE,
409 407 flags=flags,
410 408 payload=chunk)
411 409
412 410 flags |= FLAG_COMMAND_RESPONSE_CONTINUATION
413 411
414 flags ^= FLAG_COMMAND_RESPONSE_CONTINUATION
415 flags |= FLAG_COMMAND_RESPONSE_EOS
416 yield stream.makeframe(requestid=requestid,
412 def createcommandresponseokframe(stream, requestid):
413 overall = b''.join(cborutil.streamencode({b'status': b'ok'}))
414
415 return stream.makeframe(requestid=requestid,
417 416 typeid=FRAME_TYPE_COMMAND_RESPONSE,
418 flags=flags,
417 flags=FLAG_COMMAND_RESPONSE_CONTINUATION,
418 payload=overall)
419
420 def createcommandresponseeosframe(stream, requestid):
421 """Create an empty payload frame representing command end-of-stream."""
422 return stream.makeframe(requestid=requestid,
423 typeid=FRAME_TYPE_COMMAND_RESPONSE,
424 flags=FLAG_COMMAND_RESPONSE_EOS,
419 425 payload=b'')
420 426
421 427 def createcommanderrorresponse(stream, requestid, message, args=None):
422 428 # TODO should this be using a list of {'msg': ..., 'args': {}} so atom
423 429 # formatting works consistently?
424 430 m = {
425 431 b'status': b'error',
426 432 b'error': {
427 433 b'message': message,
428 434 }
429 435 }
430 436
431 437 if args:
432 438 m[b'error'][b'args'] = args
433 439
434 440 overall = b''.join(cborutil.streamencode(m))
435 441
436 442 yield stream.makeframe(requestid=requestid,
437 443 typeid=FRAME_TYPE_COMMAND_RESPONSE,
438 444 flags=FLAG_COMMAND_RESPONSE_EOS,
439 445 payload=overall)
440 446
441 447 def createerrorframe(stream, requestid, msg, errtype):
442 448 # TODO properly handle frame size limits.
443 449 assert len(msg) <= DEFAULT_MAX_FRAME_SIZE
444 450
445 451 payload = b''.join(cborutil.streamencode({
446 452 b'type': errtype,
447 453 b'message': [{b'msg': msg}],
448 454 }))
449 455
450 456 yield stream.makeframe(requestid=requestid,
451 457 typeid=FRAME_TYPE_ERROR_RESPONSE,
452 458 flags=0,
453 459 payload=payload)
454 460
455 461 def createtextoutputframe(stream, requestid, atoms,
456 462 maxframesize=DEFAULT_MAX_FRAME_SIZE):
457 463 """Create a text output frame to render text to people.
458 464
459 465 ``atoms`` is a 3-tuple of (formatting string, args, labels).
460 466
461 467 The formatting string contains ``%s`` tokens to be replaced by the
462 468 corresponding indexed entry in ``args``. ``labels`` is an iterable of
463 469 formatters to be applied at rendering time. In terms of the ``ui``
464 470 class, each atom corresponds to a ``ui.write()``.
465 471 """
466 472 atomdicts = []
467 473
468 474 for (formatting, args, labels) in atoms:
469 475 # TODO look for localstr, other types here?
470 476
471 477 if not isinstance(formatting, bytes):
472 478 raise ValueError('must use bytes formatting strings')
473 479 for arg in args:
474 480 if not isinstance(arg, bytes):
475 481 raise ValueError('must use bytes for arguments')
476 482 for label in labels:
477 483 if not isinstance(label, bytes):
478 484 raise ValueError('must use bytes for labels')
479 485
480 486 # Formatting string must be ASCII.
481 487 formatting = formatting.decode(r'ascii', r'replace').encode(r'ascii')
482 488
483 489 # Arguments must be UTF-8.
484 490 args = [a.decode(r'utf-8', r'replace').encode(r'utf-8') for a in args]
485 491
486 492 # Labels must be ASCII.
487 493 labels = [l.decode(r'ascii', r'strict').encode(r'ascii')
488 494 for l in labels]
489 495
490 496 atom = {b'msg': formatting}
491 497 if args:
492 498 atom[b'args'] = args
493 499 if labels:
494 500 atom[b'labels'] = labels
495 501
496 502 atomdicts.append(atom)
497 503
498 504 payload = b''.join(cborutil.streamencode(atomdicts))
499 505
500 506 if len(payload) > maxframesize:
501 507 raise ValueError('cannot encode data in a single frame')
502 508
503 509 yield stream.makeframe(requestid=requestid,
504 510 typeid=FRAME_TYPE_TEXT_OUTPUT,
505 511 flags=0,
506 512 payload=payload)
507 513
508 514 class stream(object):
509 515 """Represents a logical unidirectional series of frames."""
510 516
511 517 def __init__(self, streamid, active=False):
512 518 self.streamid = streamid
513 519 self._active = active
514 520
515 521 def makeframe(self, requestid, typeid, flags, payload):
516 522 """Create a frame to be sent out over this stream.
517 523
518 524 Only returns the frame instance. Does not actually send it.
519 525 """
520 526 streamflags = 0
521 527 if not self._active:
522 528 streamflags |= STREAM_FLAG_BEGIN_STREAM
523 529 self._active = True
524 530
525 531 return makeframe(requestid, self.streamid, streamflags, typeid, flags,
526 532 payload)
527 533
528 534 def ensureserverstream(stream):
529 535 if stream.streamid % 2:
530 536 raise error.ProgrammingError('server should only write to even '
531 537 'numbered streams; %d is not even' %
532 538 stream.streamid)
533 539
534 540 class serverreactor(object):
535 541 """Holds state of a server handling frame-based protocol requests.
536 542
537 543 This class is the "brain" of the unified frame-based protocol server
538 544 component. While the protocol is stateless from the perspective of
539 545 requests/commands, something needs to track which frames have been
540 546 received, what frames to expect, etc. This class is that thing.
541 547
542 548 Instances are modeled as a state machine of sorts. Instances are also
543 549 reactionary to external events. The point of this class is to encapsulate
544 550 the state of the connection and the exchange of frames, not to perform
545 551 work. Instead, callers tell this class when something occurs, like a
546 552 frame arriving. If that activity is worthy of a follow-up action (say
547 553 *run a command*), the return value of that handler will say so.
548 554
549 555 I/O and CPU intensive operations are purposefully delegated outside of
550 556 this class.
551 557
552 558 Consumers are expected to tell instances when events occur. They do so by
553 559 calling the various ``on*`` methods. These methods return a 2-tuple
554 560 describing any follow-up action(s) to take. The first element is the
555 561 name of an action to perform. The second is a data structure (usually
556 562 a dict) specific to that action that contains more information. e.g.
557 563 if the server wants to send frames back to the client, the data structure
558 564 will contain a reference to those frames.
559 565
560 566 Valid actions that consumers can be instructed to take are:
561 567
562 568 sendframes
563 569 Indicates that frames should be sent to the client. The ``framegen``
564 570 key contains a generator of frames that should be sent. The server
565 571 assumes that all frames are sent to the client.
566 572
567 573 error
568 574 Indicates that an error occurred. Consumer should probably abort.
569 575
570 576 runcommand
571 577 Indicates that the consumer should run a wire protocol command. Details
572 578 of the command to run are given in the data structure.
573 579
574 580 wantframe
575 581 Indicates that nothing of interest happened and the server is waiting on
576 582 more frames from the client before anything interesting can be done.
577 583
578 584 noop
579 585 Indicates no additional action is required.
580 586
581 587 Known Issues
582 588 ------------
583 589
584 590 There are no limits to the number of partially received commands or their
585 591 size. A malicious client could stream command request data and exhaust the
586 592 server's memory.
587 593
588 594 Partially received commands are not acted upon when end of input is
589 595 reached. Should the server error if it receives a partial request?
590 596 Should the client send a message to abort a partially transmitted request
591 597 to facilitate graceful shutdown?
592 598
593 599 Active requests that haven't been responded to aren't tracked. This means
594 600 that if we receive a command and instruct its dispatch, another command
595 601 with its request ID can come in over the wire and there will be a race
596 602 between who responds to what.
597 603 """
598 604
599 605 def __init__(self, deferoutput=False):
600 606 """Construct a new server reactor.
601 607
602 608 ``deferoutput`` can be used to indicate that no output frames should be
603 609 instructed to be sent until input has been exhausted. In this mode,
604 610 events that would normally generate output frames (such as a command
605 611 response being ready) will instead defer instructing the consumer to
606 612 send those frames. This is useful for half-duplex transports where the
607 613 sender cannot receive until all data has been transmitted.
608 614 """
609 615 self._deferoutput = deferoutput
610 616 self._state = 'idle'
611 617 self._nextoutgoingstreamid = 2
612 618 self._bufferedframegens = []
613 619 # stream id -> stream instance for all active streams from the client.
614 620 self._incomingstreams = {}
615 621 self._outgoingstreams = {}
616 622 # request id -> dict of commands that are actively being received.
617 623 self._receivingcommands = {}
618 624 # Request IDs that have been received and are actively being processed.
619 625 # Once all output for a request has been sent, it is removed from this
620 626 # set.
621 627 self._activecommands = set()
622 628
623 629 def onframerecv(self, frame):
624 630 """Process a frame that has been received off the wire.
625 631
626 632 Returns a dict with an ``action`` key that details what action,
627 633 if any, the consumer should take next.
628 634 """
629 635 if not frame.streamid % 2:
630 636 self._state = 'errored'
631 637 return self._makeerrorresult(
632 638 _('received frame with even numbered stream ID: %d') %
633 639 frame.streamid)
634 640
635 641 if frame.streamid not in self._incomingstreams:
636 642 if not frame.streamflags & STREAM_FLAG_BEGIN_STREAM:
637 643 self._state = 'errored'
638 644 return self._makeerrorresult(
639 645 _('received frame on unknown inactive stream without '
640 646 'beginning of stream flag set'))
641 647
642 648 self._incomingstreams[frame.streamid] = stream(frame.streamid)
643 649
644 650 if frame.streamflags & STREAM_FLAG_ENCODING_APPLIED:
645 651 # TODO handle decoding frames
646 652 self._state = 'errored'
647 653 raise error.ProgrammingError('support for decoding stream payloads '
648 654 'not yet implemented')
649 655
650 656 if frame.streamflags & STREAM_FLAG_END_STREAM:
651 657 del self._incomingstreams[frame.streamid]
652 658
653 659 handlers = {
654 660 'idle': self._onframeidle,
655 661 'command-receiving': self._onframecommandreceiving,
656 662 'errored': self._onframeerrored,
657 663 }
658 664
659 665 meth = handlers.get(self._state)
660 666 if not meth:
661 667 raise error.ProgrammingError('unhandled state: %s' % self._state)
662 668
663 669 return meth(frame)
664 670
665 671 def oncommandresponseready(self, stream, requestid, data):
666 672 """Signal that a bytes response is ready to be sent to the client.
667 673
668 674 The raw bytes response is passed as an argument.
669 675 """
670 676 ensureserverstream(stream)
671 677
672 678 def sendframes():
673 679 for frame in createcommandresponseframesfrombytes(stream, requestid,
674 680 data):
675 681 yield frame
676 682
677 683 self._activecommands.remove(requestid)
678 684
679 685 result = sendframes()
680 686
681 687 if self._deferoutput:
682 688 self._bufferedframegens.append(result)
683 689 return 'noop', {}
684 690 else:
685 691 return 'sendframes', {
686 692 'framegen': result,
687 693 }
688 694
689 def oncommandresponsereadygen(self, stream, requestid, gen):
690 """Signal that a bytes response is ready, with data as a generator."""
695 def oncommandresponsereadyobjects(self, stream, requestid, objs):
696 """Signal that objects are ready to be sent to the client.
697
698 ``objs`` is an iterable of objects (typically a generator) that will
699 be encoded via CBOR and added to frames, which will be sent to the
700 client.
701 """
691 702 ensureserverstream(stream)
692 703
704 # We need to take care over exception handling. Uncaught exceptions
705 # when generating frames could lead to premature end of the frame
706 # stream and the possibility of the server or client process getting
707 # in a bad state.
708 #
709 # Keep in mind that if ``objs`` is a generator, advancing it could
710 # raise exceptions that originated in e.g. wire protocol command
711 # functions. That is why we differentiate between exceptions raised
712 # when iterating versus other exceptions that occur.
713 #
714 # In all cases, when the function finishes, the request is fully
715 # handled and no new frames for it should be seen.
716
693 717 def sendframes():
694 for frame in createbytesresponseframesfromgen(stream, requestid,
695 gen):
718 emitted = False
719 while True:
720 try:
721 o = next(objs)
722 except StopIteration:
723 if emitted:
724 yield createcommandresponseeosframe(stream, requestid)
725 break
726
727 except error.WireprotoCommandError as e:
728 for frame in createcommanderrorresponse(
729 stream, requestid, e.message, e.messageargs):
730 yield frame
731 break
732
733 except Exception as e:
734 for frame in createerrorframe(stream, requestid,
735 '%s' % e,
736 errtype='server'):
696 737 yield frame
697 738
739 break
740
741 try:
742 if not emitted:
743 yield createcommandresponseokframe(stream, requestid)
744 emitted = True
745
746 # TODO buffer chunks so emitted frame payloads can be
747 # larger.
748 for frame in createbytesresponseframesfromgen(
749 stream, requestid, cborutil.streamencode(o)):
750 yield frame
751 except Exception as e:
752 for frame in createerrorframe(stream, requestid,
753 '%s' % e,
754 errtype='server'):
755 yield frame
756
757 break
758
698 759 self._activecommands.remove(requestid)
699 760
700 761 return self._handlesendframes(sendframes())
701 762
702 763 def oninputeof(self):
703 764 """Signals that end of input has been received.
704 765
705 766 No more frames will be received. All pending activity should be
706 767 completed.
707 768 """
708 769 # TODO should we do anything about in-flight commands?
709 770
710 771 if not self._deferoutput or not self._bufferedframegens:
711 772 return 'noop', {}
712 773
713 774 # If we buffered all our responses, emit those.
714 775 def makegen():
715 776 for gen in self._bufferedframegens:
716 777 for frame in gen:
717 778 yield frame
718 779
719 780 return 'sendframes', {
720 781 'framegen': makegen(),
721 782 }
722 783
723 784 def _handlesendframes(self, framegen):
724 785 if self._deferoutput:
725 786 self._bufferedframegens.append(framegen)
726 787 return 'noop', {}
727 788 else:
728 789 return 'sendframes', {
729 790 'framegen': framegen,
730 791 }
731 792
732 793 def onservererror(self, stream, requestid, msg):
733 794 ensureserverstream(stream)
734 795
735 796 def sendframes():
736 797 for frame in createerrorframe(stream, requestid, msg,
737 798 errtype='server'):
738 799 yield frame
739 800
740 801 self._activecommands.remove(requestid)
741 802
742 803 return self._handlesendframes(sendframes())
743 804
744 805 def oncommanderror(self, stream, requestid, message, args=None):
745 806 """Called when a command encountered an error before sending output."""
746 807 ensureserverstream(stream)
747 808
748 809 def sendframes():
749 810 for frame in createcommanderrorresponse(stream, requestid, message,
750 811 args):
751 812 yield frame
752 813
753 814 self._activecommands.remove(requestid)
754 815
755 816 return self._handlesendframes(sendframes())
756 817
757 818 def makeoutputstream(self):
758 819 """Create a stream to be used for sending data to the client."""
759 820 streamid = self._nextoutgoingstreamid
760 821 self._nextoutgoingstreamid += 2
761 822
762 823 s = stream(streamid)
763 824 self._outgoingstreams[streamid] = s
764 825
765 826 return s
766 827
767 828 def _makeerrorresult(self, msg):
768 829 return 'error', {
769 830 'message': msg,
770 831 }
771 832
772 833 def _makeruncommandresult(self, requestid):
773 834 entry = self._receivingcommands[requestid]
774 835
775 836 if not entry['requestdone']:
776 837 self._state = 'errored'
777 838 raise error.ProgrammingError('should not be called without '
778 839 'requestdone set')
779 840
780 841 del self._receivingcommands[requestid]
781 842
782 843 if self._receivingcommands:
783 844 self._state = 'command-receiving'
784 845 else:
785 846 self._state = 'idle'
786 847
787 848 # Decode the payloads as CBOR.
788 849 entry['payload'].seek(0)
789 850 request = cborutil.decodeall(entry['payload'].getvalue())[0]
790 851
791 852 if b'name' not in request:
792 853 self._state = 'errored'
793 854 return self._makeerrorresult(
794 855 _('command request missing "name" field'))
795 856
796 857 if b'args' not in request:
797 858 request[b'args'] = {}
798 859
799 860 assert requestid not in self._activecommands
800 861 self._activecommands.add(requestid)
801 862
802 863 return 'runcommand', {
803 864 'requestid': requestid,
804 865 'command': request[b'name'],
805 866 'args': request[b'args'],
806 867 'data': entry['data'].getvalue() if entry['data'] else None,
807 868 }
808 869
809 870 def _makewantframeresult(self):
810 871 return 'wantframe', {
811 872 'state': self._state,
812 873 }
813 874
814 875 def _validatecommandrequestframe(self, frame):
815 876 new = frame.flags & FLAG_COMMAND_REQUEST_NEW
816 877 continuation = frame.flags & FLAG_COMMAND_REQUEST_CONTINUATION
817 878
818 879 if new and continuation:
819 880 self._state = 'errored'
820 881 return self._makeerrorresult(
821 882 _('received command request frame with both new and '
822 883 'continuation flags set'))
823 884
824 885 if not new and not continuation:
825 886 self._state = 'errored'
826 887 return self._makeerrorresult(
827 888 _('received command request frame with neither new nor '
828 889 'continuation flags set'))
829 890
830 891 def _onframeidle(self, frame):
831 892 # The only frame type that should be received in this state is a
832 893 # command request.
833 894 if frame.typeid != FRAME_TYPE_COMMAND_REQUEST:
834 895 self._state = 'errored'
835 896 return self._makeerrorresult(
836 897 _('expected command request frame; got %d') % frame.typeid)
837 898
838 899 res = self._validatecommandrequestframe(frame)
839 900 if res:
840 901 return res
841 902
842 903 if frame.requestid in self._receivingcommands:
843 904 self._state = 'errored'
844 905 return self._makeerrorresult(
845 906 _('request with ID %d already received') % frame.requestid)
846 907
847 908 if frame.requestid in self._activecommands:
848 909 self._state = 'errored'
849 910 return self._makeerrorresult(
850 911 _('request with ID %d is already active') % frame.requestid)
851 912
852 913 new = frame.flags & FLAG_COMMAND_REQUEST_NEW
853 914 moreframes = frame.flags & FLAG_COMMAND_REQUEST_MORE_FRAMES
854 915 expectingdata = frame.flags & FLAG_COMMAND_REQUEST_EXPECT_DATA
855 916
856 917 if not new:
857 918 self._state = 'errored'
858 919 return self._makeerrorresult(
859 920 _('received command request frame without new flag set'))
860 921
861 922 payload = util.bytesio()
862 923 payload.write(frame.payload)
863 924
864 925 self._receivingcommands[frame.requestid] = {
865 926 'payload': payload,
866 927 'data': None,
867 928 'requestdone': not moreframes,
868 929 'expectingdata': bool(expectingdata),
869 930 }
870 931
871 932 # This is the final frame for this request. Dispatch it.
872 933 if not moreframes and not expectingdata:
873 934 return self._makeruncommandresult(frame.requestid)
874 935
875 936 assert moreframes or expectingdata
876 937 self._state = 'command-receiving'
877 938 return self._makewantframeresult()
878 939
879 940 def _onframecommandreceiving(self, frame):
880 941 if frame.typeid == FRAME_TYPE_COMMAND_REQUEST:
881 942 # Process new command requests as such.
882 943 if frame.flags & FLAG_COMMAND_REQUEST_NEW:
883 944 return self._onframeidle(frame)
884 945
885 946 res = self._validatecommandrequestframe(frame)
886 947 if res:
887 948 return res
888 949
889 950 # All other frames should be related to a command that is currently
890 951 # receiving but is not active.
891 952 if frame.requestid in self._activecommands:
892 953 self._state = 'errored'
893 954 return self._makeerrorresult(
894 955 _('received frame for request that is still active: %d') %
895 956 frame.requestid)
896 957
897 958 if frame.requestid not in self._receivingcommands:
898 959 self._state = 'errored'
899 960 return self._makeerrorresult(
900 961 _('received frame for request that is not receiving: %d') %
901 962 frame.requestid)
902 963
903 964 entry = self._receivingcommands[frame.requestid]
904 965
905 966 if frame.typeid == FRAME_TYPE_COMMAND_REQUEST:
906 967 moreframes = frame.flags & FLAG_COMMAND_REQUEST_MORE_FRAMES
907 968 expectingdata = bool(frame.flags & FLAG_COMMAND_REQUEST_EXPECT_DATA)
908 969
909 970 if entry['requestdone']:
910 971 self._state = 'errored'
911 972 return self._makeerrorresult(
912 973 _('received command request frame when request frames '
913 974 'were supposedly done'))
914 975
915 976 if expectingdata != entry['expectingdata']:
916 977 self._state = 'errored'
917 978 return self._makeerrorresult(
918 979 _('mismatch between expect data flag and previous frame'))
919 980
920 981 entry['payload'].write(frame.payload)
921 982
922 983 if not moreframes:
923 984 entry['requestdone'] = True
924 985
925 986 if not moreframes and not expectingdata:
926 987 return self._makeruncommandresult(frame.requestid)
927 988
928 989 return self._makewantframeresult()
929 990
930 991 elif frame.typeid == FRAME_TYPE_COMMAND_DATA:
931 992 if not entry['expectingdata']:
932 993 self._state = 'errored'
933 994 return self._makeerrorresult(_(
934 995 'received command data frame for request that is not '
935 996 'expecting data: %d') % frame.requestid)
936 997
937 998 if entry['data'] is None:
938 999 entry['data'] = util.bytesio()
939 1000
940 1001 return self._handlecommanddataframe(frame, entry)
941 1002 else:
942 1003 self._state = 'errored'
943 1004 return self._makeerrorresult(_(
944 1005 'received unexpected frame type: %d') % frame.typeid)
945 1006
946 1007 def _handlecommanddataframe(self, frame, entry):
947 1008 assert frame.typeid == FRAME_TYPE_COMMAND_DATA
948 1009
949 1010 # TODO support streaming data instead of buffering it.
950 1011 entry['data'].write(frame.payload)
951 1012
952 1013 if frame.flags & FLAG_COMMAND_DATA_CONTINUATION:
953 1014 return self._makewantframeresult()
954 1015 elif frame.flags & FLAG_COMMAND_DATA_EOS:
955 1016 entry['data'].seek(0)
956 1017 return self._makeruncommandresult(frame.requestid)
957 1018 else:
958 1019 self._state = 'errored'
959 1020 return self._makeerrorresult(_('command data frame without '
960 1021 'flags'))
961 1022
962 1023 def _onframeerrored(self, frame):
963 1024 return self._makeerrorresult(_('server already errored'))
964 1025
965 1026 class commandrequest(object):
966 1027 """Represents a request to run a command."""
967 1028
968 1029 def __init__(self, requestid, name, args, datafh=None):
969 1030 self.requestid = requestid
970 1031 self.name = name
971 1032 self.args = args
972 1033 self.datafh = datafh
973 1034 self.state = 'pending'
974 1035
975 1036 class clientreactor(object):
976 1037 """Holds state of a client issuing frame-based protocol requests.
977 1038
978 1039 This is like ``serverreactor`` but for client-side state.
979 1040
980 1041 Each instance is bound to the lifetime of a connection. For persistent
981 1042 connection transports using e.g. TCP sockets and speaking the raw
982 1043 framing protocol, there will be a single instance for the lifetime of
983 1044 the TCP socket. For transports where there are multiple discrete
984 1045 interactions (say tunneled within in HTTP request), there will be a
985 1046 separate instance for each distinct interaction.
986 1047 """
987 1048 def __init__(self, hasmultiplesend=False, buffersends=True):
988 1049 """Create a new instance.
989 1050
990 1051 ``hasmultiplesend`` indicates whether multiple sends are supported
991 1052 by the transport. When True, it is possible to send commands immediately
992 1053 instead of buffering until the caller signals an intent to finish a
993 1054 send operation.
994 1055
995 1056 ``buffercommands`` indicates whether sends should be buffered until the
996 1057 last request has been issued.
997 1058 """
998 1059 self._hasmultiplesend = hasmultiplesend
999 1060 self._buffersends = buffersends
1000 1061
1001 1062 self._canissuecommands = True
1002 1063 self._cansend = True
1003 1064
1004 1065 self._nextrequestid = 1
1005 1066 # We only support a single outgoing stream for now.
1006 1067 self._outgoingstream = stream(1)
1007 1068 self._pendingrequests = collections.deque()
1008 1069 self._activerequests = {}
1009 1070 self._incomingstreams = {}
1010 1071
1011 1072 def callcommand(self, name, args, datafh=None):
1012 1073 """Request that a command be executed.
1013 1074
1014 1075 Receives the command name, a dict of arguments to pass to the command,
1015 1076 and an optional file object containing the raw data for the command.
1016 1077
1017 1078 Returns a 3-tuple of (request, action, action data).
1018 1079 """
1019 1080 if not self._canissuecommands:
1020 1081 raise error.ProgrammingError('cannot issue new commands')
1021 1082
1022 1083 requestid = self._nextrequestid
1023 1084 self._nextrequestid += 2
1024 1085
1025 1086 request = commandrequest(requestid, name, args, datafh=datafh)
1026 1087
1027 1088 if self._buffersends:
1028 1089 self._pendingrequests.append(request)
1029 1090 return request, 'noop', {}
1030 1091 else:
1031 1092 if not self._cansend:
1032 1093 raise error.ProgrammingError('sends cannot be performed on '
1033 1094 'this instance')
1034 1095
1035 1096 if not self._hasmultiplesend:
1036 1097 self._cansend = False
1037 1098 self._canissuecommands = False
1038 1099
1039 1100 return request, 'sendframes', {
1040 1101 'framegen': self._makecommandframes(request),
1041 1102 }
1042 1103
1043 1104 def flushcommands(self):
1044 1105 """Request that all queued commands be sent.
1045 1106
1046 1107 If any commands are buffered, this will instruct the caller to send
1047 1108 them over the wire. If no commands are buffered it instructs the client
1048 1109 to no-op.
1049 1110
1050 1111 If instances aren't configured for multiple sends, no new command
1051 1112 requests are allowed after this is called.
1052 1113 """
1053 1114 if not self._pendingrequests:
1054 1115 return 'noop', {}
1055 1116
1056 1117 if not self._cansend:
1057 1118 raise error.ProgrammingError('sends cannot be performed on this '
1058 1119 'instance')
1059 1120
1060 1121 # If the instance only allows sending once, mark that we have fired
1061 1122 # our one shot.
1062 1123 if not self._hasmultiplesend:
1063 1124 self._canissuecommands = False
1064 1125 self._cansend = False
1065 1126
1066 1127 def makeframes():
1067 1128 while self._pendingrequests:
1068 1129 request = self._pendingrequests.popleft()
1069 1130 for frame in self._makecommandframes(request):
1070 1131 yield frame
1071 1132
1072 1133 return 'sendframes', {
1073 1134 'framegen': makeframes(),
1074 1135 }
1075 1136
1076 1137 def _makecommandframes(self, request):
1077 1138 """Emit frames to issue a command request.
1078 1139
1079 1140 As a side-effect, update request accounting to reflect its changed
1080 1141 state.
1081 1142 """
1082 1143 self._activerequests[request.requestid] = request
1083 1144 request.state = 'sending'
1084 1145
1085 1146 res = createcommandframes(self._outgoingstream,
1086 1147 request.requestid,
1087 1148 request.name,
1088 1149 request.args,
1089 1150 request.datafh)
1090 1151
1091 1152 for frame in res:
1092 1153 yield frame
1093 1154
1094 1155 request.state = 'sent'
1095 1156
1096 1157 def onframerecv(self, frame):
1097 1158 """Process a frame that has been received off the wire.
1098 1159
1099 1160 Returns a 2-tuple of (action, meta) describing further action the
1100 1161 caller needs to take as a result of receiving this frame.
1101 1162 """
1102 1163 if frame.streamid % 2:
1103 1164 return 'error', {
1104 1165 'message': (
1105 1166 _('received frame with odd numbered stream ID: %d') %
1106 1167 frame.streamid),
1107 1168 }
1108 1169
1109 1170 if frame.streamid not in self._incomingstreams:
1110 1171 if not frame.streamflags & STREAM_FLAG_BEGIN_STREAM:
1111 1172 return 'error', {
1112 1173 'message': _('received frame on unknown stream '
1113 1174 'without beginning of stream flag set'),
1114 1175 }
1115 1176
1116 1177 self._incomingstreams[frame.streamid] = stream(frame.streamid)
1117 1178
1118 1179 if frame.streamflags & STREAM_FLAG_ENCODING_APPLIED:
1119 1180 raise error.ProgrammingError('support for decoding stream '
1120 1181 'payloads not yet implemneted')
1121 1182
1122 1183 if frame.streamflags & STREAM_FLAG_END_STREAM:
1123 1184 del self._incomingstreams[frame.streamid]
1124 1185
1125 1186 if frame.requestid not in self._activerequests:
1126 1187 return 'error', {
1127 1188 'message': (_('received frame for inactive request ID: %d') %
1128 1189 frame.requestid),
1129 1190 }
1130 1191
1131 1192 request = self._activerequests[frame.requestid]
1132 1193 request.state = 'receiving'
1133 1194
1134 1195 handlers = {
1135 1196 FRAME_TYPE_COMMAND_RESPONSE: self._oncommandresponseframe,
1136 1197 FRAME_TYPE_ERROR_RESPONSE: self._onerrorresponseframe,
1137 1198 }
1138 1199
1139 1200 meth = handlers.get(frame.typeid)
1140 1201 if not meth:
1141 1202 raise error.ProgrammingError('unhandled frame type: %d' %
1142 1203 frame.typeid)
1143 1204
1144 1205 return meth(request, frame)
1145 1206
1146 1207 def _oncommandresponseframe(self, request, frame):
1147 1208 if frame.flags & FLAG_COMMAND_RESPONSE_EOS:
1148 1209 request.state = 'received'
1149 1210 del self._activerequests[request.requestid]
1150 1211
1151 1212 return 'responsedata', {
1152 1213 'request': request,
1153 1214 'expectmore': frame.flags & FLAG_COMMAND_RESPONSE_CONTINUATION,
1154 1215 'eos': frame.flags & FLAG_COMMAND_RESPONSE_EOS,
1155 1216 'data': frame.payload,
1156 1217 }
1157 1218
1158 1219 def _onerrorresponseframe(self, request, frame):
1159 1220 request.state = 'errored'
1160 1221 del self._activerequests[request.requestid]
1161 1222
1162 1223 # The payload should be a CBOR map.
1163 1224 m = cborutil.decodeall(frame.payload)[0]
1164 1225
1165 1226 return 'error', {
1166 1227 'request': request,
1167 1228 'type': m['type'],
1168 1229 'message': m['message'],
1169 1230 }
@@ -1,375 +1,354
1 1 # Copyright 2018 Gregory Szorc <gregory.szorc@gmail.com>
2 2 #
3 3 # This software may be used and distributed according to the terms of the
4 4 # GNU General Public License version 2 or any later version.
5 5
6 6 from __future__ import absolute_import
7 7
8 8 from .node import (
9 9 bin,
10 10 hex,
11 11 )
12 12 from .i18n import _
13 13 from . import (
14 14 error,
15 15 util,
16 16 )
17 17 from .utils import (
18 18 interfaceutil,
19 19 )
20 20
21 21 # Names of the SSH protocol implementations.
22 22 SSHV1 = 'ssh-v1'
23 23 # These are advertised over the wire. Increment the counters at the end
24 24 # to reflect BC breakages.
25 25 SSHV2 = 'exp-ssh-v2-0001'
26 26 HTTP_WIREPROTO_V2 = 'exp-http-v2-0001'
27 27
28 28 # All available wire protocol transports.
29 29 TRANSPORTS = {
30 30 SSHV1: {
31 31 'transport': 'ssh',
32 32 'version': 1,
33 33 },
34 34 SSHV2: {
35 35 'transport': 'ssh',
36 36 # TODO mark as version 2 once all commands are implemented.
37 37 'version': 1,
38 38 },
39 39 'http-v1': {
40 40 'transport': 'http',
41 41 'version': 1,
42 42 },
43 43 HTTP_WIREPROTO_V2: {
44 44 'transport': 'http',
45 45 'version': 2,
46 46 }
47 47 }
48 48
49 49 class bytesresponse(object):
50 50 """A wire protocol response consisting of raw bytes."""
51 51 def __init__(self, data):
52 52 self.data = data
53 53
54 54 class ooberror(object):
55 55 """wireproto reply: failure of a batch of operation
56 56
57 57 Something failed during a batch call. The error message is stored in
58 58 `self.message`.
59 59 """
60 60 def __init__(self, message):
61 61 self.message = message
62 62
63 63 class pushres(object):
64 64 """wireproto reply: success with simple integer return
65 65
66 66 The call was successful and returned an integer contained in `self.res`.
67 67 """
68 68 def __init__(self, res, output):
69 69 self.res = res
70 70 self.output = output
71 71
72 72 class pusherr(object):
73 73 """wireproto reply: failure
74 74
75 75 The call failed. The `self.res` attribute contains the error message.
76 76 """
77 77 def __init__(self, res, output):
78 78 self.res = res
79 79 self.output = output
80 80
81 81 class streamres(object):
82 82 """wireproto reply: binary stream
83 83
84 84 The call was successful and the result is a stream.
85 85
86 86 Accepts a generator containing chunks of data to be sent to the client.
87 87
88 88 ``prefer_uncompressed`` indicates that the data is expected to be
89 89 uncompressable and that the stream should therefore use the ``none``
90 90 engine.
91 91 """
92 92 def __init__(self, gen=None, prefer_uncompressed=False):
93 93 self.gen = gen
94 94 self.prefer_uncompressed = prefer_uncompressed
95 95
96 96 class streamreslegacy(object):
97 97 """wireproto reply: uncompressed binary stream
98 98
99 99 The call was successful and the result is a stream.
100 100
101 101 Accepts a generator containing chunks of data to be sent to the client.
102 102
103 103 Like ``streamres``, but sends an uncompressed data for "version 1" clients
104 104 using the application/mercurial-0.1 media type.
105 105 """
106 106 def __init__(self, gen=None):
107 107 self.gen = gen
108 108
109 class cborresponse(object):
110 """Encode the response value as CBOR."""
111 def __init__(self, v):
112 self.value = v
113
114 class v2errorresponse(object):
115 """Represents a command error for version 2 transports."""
116 def __init__(self, message, args=None):
117 self.message = message
118 self.args = args
119
120 class v2streamingresponse(object):
121 """A response whose data is supplied by a generator.
122
123 The generator can either consist of data structures to CBOR
124 encode or a stream of already-encoded bytes.
125 """
126 def __init__(self, gen, compressible=True):
127 self.gen = gen
128 self.compressible = compressible
129
130 109 # list of nodes encoding / decoding
131 110 def decodelist(l, sep=' '):
132 111 if l:
133 112 return [bin(v) for v in l.split(sep)]
134 113 return []
135 114
136 115 def encodelist(l, sep=' '):
137 116 try:
138 117 return sep.join(map(hex, l))
139 118 except TypeError:
140 119 raise
141 120
142 121 # batched call argument encoding
143 122
144 123 def escapebatcharg(plain):
145 124 return (plain
146 125 .replace(':', ':c')
147 126 .replace(',', ':o')
148 127 .replace(';', ':s')
149 128 .replace('=', ':e'))
150 129
151 130 def unescapebatcharg(escaped):
152 131 return (escaped
153 132 .replace(':e', '=')
154 133 .replace(':s', ';')
155 134 .replace(':o', ',')
156 135 .replace(':c', ':'))
157 136
158 137 # mapping of options accepted by getbundle and their types
159 138 #
160 139 # Meant to be extended by extensions. It is extensions responsibility to ensure
161 140 # such options are properly processed in exchange.getbundle.
162 141 #
163 142 # supported types are:
164 143 #
165 144 # :nodes: list of binary nodes
166 145 # :csv: list of comma-separated values
167 146 # :scsv: list of comma-separated values return as set
168 147 # :plain: string with no transformation needed.
169 148 GETBUNDLE_ARGUMENTS = {
170 149 'heads': 'nodes',
171 150 'bookmarks': 'boolean',
172 151 'common': 'nodes',
173 152 'obsmarkers': 'boolean',
174 153 'phases': 'boolean',
175 154 'bundlecaps': 'scsv',
176 155 'listkeys': 'csv',
177 156 'cg': 'boolean',
178 157 'cbattempted': 'boolean',
179 158 'stream': 'boolean',
180 159 }
181 160
182 161 class baseprotocolhandler(interfaceutil.Interface):
183 162 """Abstract base class for wire protocol handlers.
184 163
185 164 A wire protocol handler serves as an interface between protocol command
186 165 handlers and the wire protocol transport layer. Protocol handlers provide
187 166 methods to read command arguments, redirect stdio for the duration of
188 167 the request, handle response types, etc.
189 168 """
190 169
191 170 name = interfaceutil.Attribute(
192 171 """The name of the protocol implementation.
193 172
194 173 Used for uniquely identifying the transport type.
195 174 """)
196 175
197 176 def getargs(args):
198 177 """return the value for arguments in <args>
199 178
200 179 For version 1 transports, returns a list of values in the same
201 180 order they appear in ``args``. For version 2 transports, returns
202 181 a dict mapping argument name to value.
203 182 """
204 183
205 184 def getprotocaps():
206 185 """Returns the list of protocol-level capabilities of client
207 186
208 187 Returns a list of capabilities as declared by the client for
209 188 the current request (or connection for stateful protocol handlers)."""
210 189
211 190 def getpayload():
212 191 """Provide a generator for the raw payload.
213 192
214 193 The caller is responsible for ensuring that the full payload is
215 194 processed.
216 195 """
217 196
218 197 def mayberedirectstdio():
219 198 """Context manager to possibly redirect stdio.
220 199
221 200 The context manager yields a file-object like object that receives
222 201 stdout and stderr output when the context manager is active. Or it
223 202 yields ``None`` if no I/O redirection occurs.
224 203
225 204 The intent of this context manager is to capture stdio output
226 205 so it may be sent in the response. Some transports support streaming
227 206 stdio to the client in real time. For these transports, stdio output
228 207 won't be captured.
229 208 """
230 209
231 210 def client():
232 211 """Returns a string representation of this client (as bytes)."""
233 212
234 213 def addcapabilities(repo, caps):
235 214 """Adds advertised capabilities specific to this protocol.
236 215
237 216 Receives the list of capabilities collected so far.
238 217
239 218 Returns a list of capabilities. The passed in argument can be returned.
240 219 """
241 220
242 221 def checkperm(perm):
243 222 """Validate that the client has permissions to perform a request.
244 223
245 224 The argument is the permission required to proceed. If the client
246 225 doesn't have that permission, the exception should raise or abort
247 226 in a protocol specific manner.
248 227 """
249 228
250 229 class commandentry(object):
251 230 """Represents a declared wire protocol command."""
252 231 def __init__(self, func, args='', transports=None,
253 232 permission='push'):
254 233 self.func = func
255 234 self.args = args
256 235 self.transports = transports or set()
257 236 self.permission = permission
258 237
259 238 def _merge(self, func, args):
260 239 """Merge this instance with an incoming 2-tuple.
261 240
262 241 This is called when a caller using the old 2-tuple API attempts
263 242 to replace an instance. The incoming values are merged with
264 243 data not captured by the 2-tuple and a new instance containing
265 244 the union of the two objects is returned.
266 245 """
267 246 return commandentry(func, args=args, transports=set(self.transports),
268 247 permission=self.permission)
269 248
270 249 # Old code treats instances as 2-tuples. So expose that interface.
271 250 def __iter__(self):
272 251 yield self.func
273 252 yield self.args
274 253
275 254 def __getitem__(self, i):
276 255 if i == 0:
277 256 return self.func
278 257 elif i == 1:
279 258 return self.args
280 259 else:
281 260 raise IndexError('can only access elements 0 and 1')
282 261
283 262 class commanddict(dict):
284 263 """Container for registered wire protocol commands.
285 264
286 265 It behaves like a dict. But __setitem__ is overwritten to allow silent
287 266 coercion of values from 2-tuples for API compatibility.
288 267 """
289 268 def __setitem__(self, k, v):
290 269 if isinstance(v, commandentry):
291 270 pass
292 271 # Cast 2-tuples to commandentry instances.
293 272 elif isinstance(v, tuple):
294 273 if len(v) != 2:
295 274 raise ValueError('command tuples must have exactly 2 elements')
296 275
297 276 # It is common for extensions to wrap wire protocol commands via
298 277 # e.g. ``wireproto.commands[x] = (newfn, args)``. Because callers
299 278 # doing this aren't aware of the new API that uses objects to store
300 279 # command entries, we automatically merge old state with new.
301 280 if k in self:
302 281 v = self[k]._merge(v[0], v[1])
303 282 else:
304 283 # Use default values from @wireprotocommand.
305 284 v = commandentry(v[0], args=v[1],
306 285 transports=set(TRANSPORTS),
307 286 permission='push')
308 287 else:
309 288 raise ValueError('command entries must be commandentry instances '
310 289 'or 2-tuples')
311 290
312 291 return super(commanddict, self).__setitem__(k, v)
313 292
314 293 def commandavailable(self, command, proto):
315 294 """Determine if a command is available for the requested protocol."""
316 295 assert proto.name in TRANSPORTS
317 296
318 297 entry = self.get(command)
319 298
320 299 if not entry:
321 300 return False
322 301
323 302 if proto.name not in entry.transports:
324 303 return False
325 304
326 305 return True
327 306
328 307 def supportedcompengines(ui, role):
329 308 """Obtain the list of supported compression engines for a request."""
330 309 assert role in (util.CLIENTROLE, util.SERVERROLE)
331 310
332 311 compengines = util.compengines.supportedwireengines(role)
333 312
334 313 # Allow config to override default list and ordering.
335 314 if role == util.SERVERROLE:
336 315 configengines = ui.configlist('server', 'compressionengines')
337 316 config = 'server.compressionengines'
338 317 else:
339 318 # This is currently implemented mainly to facilitate testing. In most
340 319 # cases, the server should be in charge of choosing a compression engine
341 320 # because a server has the most to lose from a sub-optimal choice. (e.g.
342 321 # CPU DoS due to an expensive engine or a network DoS due to poor
343 322 # compression ratio).
344 323 configengines = ui.configlist('experimental',
345 324 'clientcompressionengines')
346 325 config = 'experimental.clientcompressionengines'
347 326
348 327 # No explicit config. Filter out the ones that aren't supposed to be
349 328 # advertised and return default ordering.
350 329 if not configengines:
351 330 attr = 'serverpriority' if role == util.SERVERROLE else 'clientpriority'
352 331 return [e for e in compengines
353 332 if getattr(e.wireprotosupport(), attr) > 0]
354 333
355 334 # If compression engines are listed in the config, assume there is a good
356 335 # reason for it (like server operators wanting to achieve specific
357 336 # performance characteristics). So fail fast if the config references
358 337 # unusable compression engines.
359 338 validnames = set(e.name() for e in compengines)
360 339 invalidnames = set(e for e in configengines if e not in validnames)
361 340 if invalidnames:
362 341 raise error.Abort(_('invalid compression engine defined in %s: %s') %
363 342 (config, ', '.join(sorted(invalidnames))))
364 343
365 344 compengines = [e for e in compengines if e.name() in configengines]
366 345 compengines = sorted(compengines,
367 346 key=lambda e: configengines.index(e.name()))
368 347
369 348 if not compengines:
370 349 raise error.Abort(_('%s config option does not specify any known '
371 350 'compression engines') % config,
372 351 hint=_('usable compression engines: %s') %
373 352 ', '.sorted(validnames))
374 353
375 354 return compengines
@@ -1,210 +1,212
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 . import (
12 12 encoding,
13 13 error,
14 14 util,
15 15 wireprotoframing,
16 16 )
17 17 from .utils import (
18 18 cborutil,
19 19 )
20 20
21 21 def formatrichmessage(atoms):
22 22 """Format an encoded message from the framing protocol."""
23 23
24 24 chunks = []
25 25
26 26 for atom in atoms:
27 27 msg = _(atom[b'msg'])
28 28
29 29 if b'args' in atom:
30 30 msg = msg % tuple(atom[b'args'])
31 31
32 32 chunks.append(msg)
33 33
34 34 return b''.join(chunks)
35 35
36 36 class commandresponse(object):
37 37 """Represents the response to a command request."""
38 38
39 39 def __init__(self, requestid, command):
40 40 self.requestid = requestid
41 41 self.command = command
42 42
43 43 self.b = util.bytesio()
44 44
45 45 def cborobjects(self):
46 46 """Obtain decoded CBOR objects from this response."""
47 47 self.b.seek(0)
48 48
49 49 for v in cborutil.decodeall(self.b.getvalue()):
50 50 yield v
51 51
52 52 class clienthandler(object):
53 53 """Object to handle higher-level client activities.
54 54
55 55 The ``clientreactor`` is used to hold low-level state about the frame-based
56 56 protocol, such as which requests and streams are active. This type is used
57 57 for higher-level operations, such as reading frames from a socket, exposing
58 58 and managing a higher-level primitive for representing command responses,
59 59 etc. This class is what peers should probably use to bridge wire activity
60 60 with the higher-level peer API.
61 61 """
62 62
63 63 def __init__(self, ui, clientreactor):
64 64 self._ui = ui
65 65 self._reactor = clientreactor
66 66 self._requests = {}
67 67 self._futures = {}
68 68 self._responses = {}
69 69
70 70 def callcommand(self, command, args, f):
71 71 """Register a request to call a command.
72 72
73 73 Returns an iterable of frames that should be sent over the wire.
74 74 """
75 75 request, action, meta = self._reactor.callcommand(command, args)
76 76
77 77 if action != 'noop':
78 78 raise error.ProgrammingError('%s not yet supported' % action)
79 79
80 80 rid = request.requestid
81 81 self._requests[rid] = request
82 82 self._futures[rid] = f
83 83 self._responses[rid] = commandresponse(rid, command)
84 84
85 85 return iter(())
86 86
87 87 def flushcommands(self):
88 88 """Flush all queued commands.
89 89
90 90 Returns an iterable of frames that should be sent over the wire.
91 91 """
92 92 action, meta = self._reactor.flushcommands()
93 93
94 94 if action != 'sendframes':
95 95 raise error.ProgrammingError('%s not yet supported' % action)
96 96
97 97 return meta['framegen']
98 98
99 99 def readframe(self, fh):
100 100 """Attempt to read and process a frame.
101 101
102 102 Returns None if no frame was read. Presumably this means EOF.
103 103 """
104 104 frame = wireprotoframing.readframe(fh)
105 105 if frame is None:
106 106 # TODO tell reactor?
107 107 return
108 108
109 109 self._ui.note(_('received %r\n') % frame)
110 110 self._processframe(frame)
111 111
112 112 return True
113 113
114 114 def _processframe(self, frame):
115 115 """Process a single read frame."""
116 116
117 117 action, meta = self._reactor.onframerecv(frame)
118 118
119 119 if action == 'error':
120 120 e = error.RepoError(meta['message'])
121 121
122 122 if frame.requestid in self._futures:
123 123 self._futures[frame.requestid].set_exception(e)
124 124 else:
125 125 raise e
126 126
127 return
128
127 129 if frame.requestid not in self._requests:
128 130 raise error.ProgrammingError(
129 131 'received frame for unknown request; this is either a bug in '
130 132 'the clientreactor not screening for this or this instance was '
131 133 'never told about this request: %r' % frame)
132 134
133 135 response = self._responses[frame.requestid]
134 136
135 137 if action == 'responsedata':
136 138 # Any failures processing this frame should bubble up to the
137 139 # future tracking the request.
138 140 try:
139 141 self._processresponsedata(frame, meta, response)
140 142 except BaseException as e:
141 143 self._futures[frame.requestid].set_exception(e)
142 144 else:
143 145 raise error.ProgrammingError(
144 146 'unhandled action from clientreactor: %s' % action)
145 147
146 148 def _processresponsedata(self, frame, meta, response):
147 149 # This buffers all data until end of stream is received. This
148 150 # is bad for performance.
149 151 # TODO make response data streamable
150 152 response.b.write(meta['data'])
151 153
152 154 if meta['eos']:
153 155 # If the command has a decoder, resolve the future to the
154 156 # decoded value. Otherwise resolve to the rich response object.
155 157 decoder = COMMAND_DECODERS.get(response.command)
156 158
157 159 # TODO consider always resolving the overall status map.
158 160 if decoder:
159 161 objs = response.cborobjects()
160 162
161 163 overall = next(objs)
162 164
163 165 if overall['status'] == 'ok':
164 166 self._futures[frame.requestid].set_result(decoder(objs))
165 167 else:
166 168 atoms = [{'msg': overall['error']['message']}]
167 169 if 'args' in overall['error']:
168 170 atoms[0]['args'] = overall['error']['args']
169 171 e = error.RepoError(formatrichmessage(atoms))
170 172 self._futures[frame.requestid].set_exception(e)
171 173 else:
172 174 self._futures[frame.requestid].set_result(response)
173 175
174 176 del self._requests[frame.requestid]
175 177 del self._futures[frame.requestid]
176 178
177 179 def decodebranchmap(objs):
178 180 # Response should be a single CBOR map of branch name to array of nodes.
179 181 bm = next(objs)
180 182
181 183 return {encoding.tolocal(k): v for k, v in bm.items()}
182 184
183 185 def decodeheads(objs):
184 186 # Array of node bytestrings.
185 187 return next(objs)
186 188
187 189 def decodeknown(objs):
188 190 # Bytestring where each byte is a 0 or 1.
189 191 raw = next(objs)
190 192
191 193 return [True if c == '1' else False for c in raw]
192 194
193 195 def decodelistkeys(objs):
194 196 # Map with bytestring keys and values.
195 197 return next(objs)
196 198
197 199 def decodelookup(objs):
198 200 return next(objs)
199 201
200 202 def decodepushkey(objs):
201 203 return next(objs)
202 204
203 205 COMMAND_DECODERS = {
204 206 'branchmap': decodebranchmap,
205 207 'heads': decodeheads,
206 208 'known': decodeknown,
207 209 'listkeys': decodelistkeys,
208 210 'lookup': decodelookup,
209 211 'pushkey': decodepushkey,
210 212 }
@@ -1,535 +1,522
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 . import (
13 13 encoding,
14 14 error,
15 15 pycompat,
16 16 streamclone,
17 17 util,
18 18 wireprotoframing,
19 19 wireprototypes,
20 20 )
21 21 from .utils import (
22 cborutil,
23 22 interfaceutil,
24 23 )
25 24
26 25 FRAMINGTYPE = b'application/mercurial-exp-framing-0005'
27 26
28 27 HTTP_WIREPROTO_V2 = wireprototypes.HTTP_WIREPROTO_V2
29 28
30 29 COMMANDS = wireprototypes.commanddict()
31 30
32 31 def handlehttpv2request(rctx, req, res, checkperm, urlparts):
33 32 from .hgweb import common as hgwebcommon
34 33
35 34 # URL space looks like: <permissions>/<command>, where <permission> can
36 35 # be ``ro`` or ``rw`` to signal read-only or read-write, respectively.
37 36
38 37 # Root URL does nothing meaningful... yet.
39 38 if not urlparts:
40 39 res.status = b'200 OK'
41 40 res.headers[b'Content-Type'] = b'text/plain'
42 41 res.setbodybytes(_('HTTP version 2 API handler'))
43 42 return
44 43
45 44 if len(urlparts) == 1:
46 45 res.status = b'404 Not Found'
47 46 res.headers[b'Content-Type'] = b'text/plain'
48 47 res.setbodybytes(_('do not know how to process %s\n') %
49 48 req.dispatchpath)
50 49 return
51 50
52 51 permission, command = urlparts[0:2]
53 52
54 53 if permission not in (b'ro', b'rw'):
55 54 res.status = b'404 Not Found'
56 55 res.headers[b'Content-Type'] = b'text/plain'
57 56 res.setbodybytes(_('unknown permission: %s') % permission)
58 57 return
59 58
60 59 if req.method != 'POST':
61 60 res.status = b'405 Method Not Allowed'
62 61 res.headers[b'Allow'] = b'POST'
63 62 res.setbodybytes(_('commands require POST requests'))
64 63 return
65 64
66 65 # At some point we'll want to use our own API instead of recycling the
67 66 # behavior of version 1 of the wire protocol...
68 67 # TODO return reasonable responses - not responses that overload the
69 68 # HTTP status line message for error reporting.
70 69 try:
71 70 checkperm(rctx, req, 'pull' if permission == b'ro' else 'push')
72 71 except hgwebcommon.ErrorResponse as e:
73 72 res.status = hgwebcommon.statusmessage(e.code, pycompat.bytestr(e))
74 73 for k, v in e.headers:
75 74 res.headers[k] = v
76 75 res.setbodybytes('permission denied')
77 76 return
78 77
79 78 # We have a special endpoint to reflect the request back at the client.
80 79 if command == b'debugreflect':
81 80 _processhttpv2reflectrequest(rctx.repo.ui, rctx.repo, req, res)
82 81 return
83 82
84 83 # Extra commands that we handle that aren't really wire protocol
85 84 # commands. Think extra hard before making this hackery available to
86 85 # extension.
87 86 extracommands = {'multirequest'}
88 87
89 88 if command not in COMMANDS and command not in extracommands:
90 89 res.status = b'404 Not Found'
91 90 res.headers[b'Content-Type'] = b'text/plain'
92 91 res.setbodybytes(_('unknown wire protocol command: %s\n') % command)
93 92 return
94 93
95 94 repo = rctx.repo
96 95 ui = repo.ui
97 96
98 97 proto = httpv2protocolhandler(req, ui)
99 98
100 99 if (not COMMANDS.commandavailable(command, proto)
101 100 and command not in extracommands):
102 101 res.status = b'404 Not Found'
103 102 res.headers[b'Content-Type'] = b'text/plain'
104 103 res.setbodybytes(_('invalid wire protocol command: %s') % command)
105 104 return
106 105
107 106 # TODO consider cases where proxies may add additional Accept headers.
108 107 if req.headers.get(b'Accept') != FRAMINGTYPE:
109 108 res.status = b'406 Not Acceptable'
110 109 res.headers[b'Content-Type'] = b'text/plain'
111 110 res.setbodybytes(_('client MUST specify Accept header with value: %s\n')
112 111 % FRAMINGTYPE)
113 112 return
114 113
115 114 if req.headers.get(b'Content-Type') != FRAMINGTYPE:
116 115 res.status = b'415 Unsupported Media Type'
117 116 # TODO we should send a response with appropriate media type,
118 117 # since client does Accept it.
119 118 res.headers[b'Content-Type'] = b'text/plain'
120 119 res.setbodybytes(_('client MUST send Content-Type header with '
121 120 'value: %s\n') % FRAMINGTYPE)
122 121 return
123 122
124 123 _processhttpv2request(ui, repo, req, res, permission, command, proto)
125 124
126 125 def _processhttpv2reflectrequest(ui, repo, req, res):
127 126 """Reads unified frame protocol request and dumps out state to client.
128 127
129 128 This special endpoint can be used to help debug the wire protocol.
130 129
131 130 Instead of routing the request through the normal dispatch mechanism,
132 131 we instead read all frames, decode them, and feed them into our state
133 132 tracker. We then dump the log of all that activity back out to the
134 133 client.
135 134 """
136 135 import json
137 136
138 137 # Reflection APIs have a history of being abused, accidentally disclosing
139 138 # sensitive data, etc. So we have a config knob.
140 139 if not ui.configbool('experimental', 'web.api.debugreflect'):
141 140 res.status = b'404 Not Found'
142 141 res.headers[b'Content-Type'] = b'text/plain'
143 142 res.setbodybytes(_('debugreflect service not available'))
144 143 return
145 144
146 145 # We assume we have a unified framing protocol request body.
147 146
148 147 reactor = wireprotoframing.serverreactor()
149 148 states = []
150 149
151 150 while True:
152 151 frame = wireprotoframing.readframe(req.bodyfh)
153 152
154 153 if not frame:
155 154 states.append(b'received: <no frame>')
156 155 break
157 156
158 157 states.append(b'received: %d %d %d %s' % (frame.typeid, frame.flags,
159 158 frame.requestid,
160 159 frame.payload))
161 160
162 161 action, meta = reactor.onframerecv(frame)
163 162 states.append(json.dumps((action, meta), sort_keys=True,
164 163 separators=(', ', ': ')))
165 164
166 165 action, meta = reactor.oninputeof()
167 166 meta['action'] = action
168 167 states.append(json.dumps(meta, sort_keys=True, separators=(', ',': ')))
169 168
170 169 res.status = b'200 OK'
171 170 res.headers[b'Content-Type'] = b'text/plain'
172 171 res.setbodybytes(b'\n'.join(states))
173 172
174 173 def _processhttpv2request(ui, repo, req, res, authedperm, reqcommand, proto):
175 174 """Post-validation handler for HTTPv2 requests.
176 175
177 176 Called when the HTTP request contains unified frame-based protocol
178 177 frames for evaluation.
179 178 """
180 179 # TODO Some HTTP clients are full duplex and can receive data before
181 180 # the entire request is transmitted. Figure out a way to indicate support
182 181 # for that so we can opt into full duplex mode.
183 182 reactor = wireprotoframing.serverreactor(deferoutput=True)
184 183 seencommand = False
185 184
186 185 outstream = reactor.makeoutputstream()
187 186
188 187 while True:
189 188 frame = wireprotoframing.readframe(req.bodyfh)
190 189 if not frame:
191 190 break
192 191
193 192 action, meta = reactor.onframerecv(frame)
194 193
195 194 if action == 'wantframe':
196 195 # Need more data before we can do anything.
197 196 continue
198 197 elif action == 'runcommand':
199 198 sentoutput = _httpv2runcommand(ui, repo, req, res, authedperm,
200 199 reqcommand, reactor, outstream,
201 200 meta, issubsequent=seencommand)
202 201
203 202 if sentoutput:
204 203 return
205 204
206 205 seencommand = True
207 206
208 207 elif action == 'error':
209 208 # TODO define proper error mechanism.
210 209 res.status = b'200 OK'
211 210 res.headers[b'Content-Type'] = b'text/plain'
212 211 res.setbodybytes(meta['message'] + b'\n')
213 212 return
214 213 else:
215 214 raise error.ProgrammingError(
216 215 'unhandled action from frame processor: %s' % action)
217 216
218 217 action, meta = reactor.oninputeof()
219 218 if action == 'sendframes':
220 219 # We assume we haven't started sending the response yet. If we're
221 220 # wrong, the response type will raise an exception.
222 221 res.status = b'200 OK'
223 222 res.headers[b'Content-Type'] = FRAMINGTYPE
224 223 res.setbodygen(meta['framegen'])
225 224 elif action == 'noop':
226 225 pass
227 226 else:
228 227 raise error.ProgrammingError('unhandled action from frame processor: %s'
229 228 % action)
230 229
231 230 def _httpv2runcommand(ui, repo, req, res, authedperm, reqcommand, reactor,
232 231 outstream, command, issubsequent):
233 232 """Dispatch a wire protocol command made from HTTPv2 requests.
234 233
235 234 The authenticated permission (``authedperm``) along with the original
236 235 command from the URL (``reqcommand``) are passed in.
237 236 """
238 237 # We already validated that the session has permissions to perform the
239 238 # actions in ``authedperm``. In the unified frame protocol, the canonical
240 239 # command to run is expressed in a frame. However, the URL also requested
241 240 # to run a specific command. We need to be careful that the command we
242 241 # run doesn't have permissions requirements greater than what was granted
243 242 # by ``authedperm``.
244 243 #
245 244 # Our rule for this is we only allow one command per HTTP request and
246 245 # that command must match the command in the URL. However, we make
247 246 # an exception for the ``multirequest`` URL. This URL is allowed to
248 247 # execute multiple commands. We double check permissions of each command
249 248 # as it is invoked to ensure there is no privilege escalation.
250 249 # TODO consider allowing multiple commands to regular command URLs
251 250 # iff each command is the same.
252 251
253 252 proto = httpv2protocolhandler(req, ui, args=command['args'])
254 253
255 254 if reqcommand == b'multirequest':
256 255 if not COMMANDS.commandavailable(command['command'], proto):
257 256 # TODO proper error mechanism
258 257 res.status = b'200 OK'
259 258 res.headers[b'Content-Type'] = b'text/plain'
260 259 res.setbodybytes(_('wire protocol command not available: %s') %
261 260 command['command'])
262 261 return True
263 262
264 263 # TODO don't use assert here, since it may be elided by -O.
265 264 assert authedperm in (b'ro', b'rw')
266 265 wirecommand = COMMANDS[command['command']]
267 266 assert wirecommand.permission in ('push', 'pull')
268 267
269 268 if authedperm == b'ro' and wirecommand.permission != 'pull':
270 269 # TODO proper error mechanism
271 270 res.status = b'403 Forbidden'
272 271 res.headers[b'Content-Type'] = b'text/plain'
273 272 res.setbodybytes(_('insufficient permissions to execute '
274 273 'command: %s') % command['command'])
275 274 return True
276 275
277 276 # TODO should we also call checkperm() here? Maybe not if we're going
278 277 # to overhaul that API. The granted scope from the URL check should
279 278 # be good enough.
280 279
281 280 else:
282 281 # Don't allow multiple commands outside of ``multirequest`` URL.
283 282 if issubsequent:
284 283 # TODO proper error mechanism
285 284 res.status = b'200 OK'
286 285 res.headers[b'Content-Type'] = b'text/plain'
287 286 res.setbodybytes(_('multiple commands cannot be issued to this '
288 287 'URL'))
289 288 return True
290 289
291 290 if reqcommand != command['command']:
292 291 # TODO define proper error mechanism
293 292 res.status = b'200 OK'
294 293 res.headers[b'Content-Type'] = b'text/plain'
295 294 res.setbodybytes(_('command in frame must match command in URL'))
296 295 return True
297 296
298 rsp = dispatch(repo, proto, command['command'])
299
300 297 res.status = b'200 OK'
301 298 res.headers[b'Content-Type'] = FRAMINGTYPE
302 299
303 # TODO consider adding a type to represent an iterable of values to
304 # be CBOR encoded.
305 if isinstance(rsp, wireprototypes.cborresponse):
306 # TODO consider calling oncommandresponsereadygen().
307 encoded = b''.join(cborutil.streamencode(rsp.value))
308 action, meta = reactor.oncommandresponseready(outstream,
309 command['requestid'],
310 encoded)
311 elif isinstance(rsp, wireprototypes.v2streamingresponse):
312 action, meta = reactor.oncommandresponsereadygen(outstream,
313 command['requestid'],
314 rsp.gen)
315 elif isinstance(rsp, wireprototypes.v2errorresponse):
316 action, meta = reactor.oncommanderror(outstream,
317 command['requestid'],
318 rsp.message,
319 rsp.args)
320 else:
300 try:
301 objs = dispatch(repo, proto, command['command'])
302
303 action, meta = reactor.oncommandresponsereadyobjects(
304 outstream, command['requestid'], objs)
305
306 except Exception as e:
321 307 action, meta = reactor.onservererror(
322 _('unhandled response type from wire proto command'))
308 outstream, command['requestid'],
309 _('exception when invoking command: %s') % e)
323 310
324 311 if action == 'sendframes':
325 312 res.setbodygen(meta['framegen'])
326 313 return True
327 314 elif action == 'noop':
328 315 return False
329 316 else:
330 317 raise error.ProgrammingError('unhandled event from reactor: %s' %
331 318 action)
332 319
333 320 def getdispatchrepo(repo, proto, command):
334 321 return repo.filtered('served')
335 322
336 323 def dispatch(repo, proto, command):
337 324 repo = getdispatchrepo(repo, proto, command)
338 325
339 326 func, spec = COMMANDS[command]
340 327 args = proto.getargs(spec)
341 328
342 329 return func(repo, proto, **args)
343 330
344 331 @interfaceutil.implementer(wireprototypes.baseprotocolhandler)
345 332 class httpv2protocolhandler(object):
346 333 def __init__(self, req, ui, args=None):
347 334 self._req = req
348 335 self._ui = ui
349 336 self._args = args
350 337
351 338 @property
352 339 def name(self):
353 340 return HTTP_WIREPROTO_V2
354 341
355 342 def getargs(self, args):
356 343 data = {}
357 344 for k, typ in args.items():
358 345 if k == '*':
359 346 raise NotImplementedError('do not support * args')
360 347 elif k in self._args:
361 348 # TODO consider validating value types.
362 349 data[k] = self._args[k]
363 350
364 351 return data
365 352
366 353 def getprotocaps(self):
367 354 # Protocol capabilities are currently not implemented for HTTP V2.
368 355 return set()
369 356
370 357 def getpayload(self):
371 358 raise NotImplementedError
372 359
373 360 @contextlib.contextmanager
374 361 def mayberedirectstdio(self):
375 362 raise NotImplementedError
376 363
377 364 def client(self):
378 365 raise NotImplementedError
379 366
380 367 def addcapabilities(self, repo, caps):
381 368 return caps
382 369
383 370 def checkperm(self, perm):
384 371 raise NotImplementedError
385 372
386 373 def httpv2apidescriptor(req, repo):
387 374 proto = httpv2protocolhandler(req, repo.ui)
388 375
389 376 return _capabilitiesv2(repo, proto)
390 377
391 378 def _capabilitiesv2(repo, proto):
392 379 """Obtain the set of capabilities for version 2 transports.
393 380
394 381 These capabilities are distinct from the capabilities for version 1
395 382 transports.
396 383 """
397 384 compression = []
398 385 for engine in wireprototypes.supportedcompengines(repo.ui, util.SERVERROLE):
399 386 compression.append({
400 387 b'name': engine.wireprotosupport().name,
401 388 })
402 389
403 390 caps = {
404 391 'commands': {},
405 392 'compression': compression,
406 393 'framingmediatypes': [FRAMINGTYPE],
407 394 }
408 395
409 396 for command, entry in COMMANDS.items():
410 397 caps['commands'][command] = {
411 398 'args': entry.args,
412 399 'permissions': [entry.permission],
413 400 }
414 401
415 402 if streamclone.allowservergeneration(repo):
416 403 caps['rawrepoformats'] = sorted(repo.requirements &
417 404 repo.supportedformats)
418 405
419 406 return proto.addcapabilities(repo, caps)
420 407
421 408 def wireprotocommand(name, args=None, permission='push'):
422 409 """Decorator to declare a wire protocol command.
423 410
424 411 ``name`` is the name of the wire protocol command being provided.
425 412
426 413 ``args`` is a dict of argument names to example values.
427 414
428 415 ``permission`` defines the permission type needed to run this command.
429 416 Can be ``push`` or ``pull``. These roughly map to read-write and read-only,
430 417 respectively. Default is to assume command requires ``push`` permissions
431 418 because otherwise commands not declaring their permissions could modify
432 419 a repository that is supposed to be read-only.
420
421 Wire protocol commands are generators of objects to be serialized and
422 sent to the client.
423
424 If a command raises an uncaught exception, this will be translated into
425 a command error.
433 426 """
434 427 transports = {k for k, v in wireprototypes.TRANSPORTS.items()
435 428 if v['version'] == 2}
436 429
437 430 if permission not in ('push', 'pull'):
438 431 raise error.ProgrammingError('invalid wire protocol permission; '
439 432 'got %s; expected "push" or "pull"' %
440 433 permission)
441 434
442 435 if args is None:
443 436 args = {}
444 437
445 438 if not isinstance(args, dict):
446 439 raise error.ProgrammingError('arguments for version 2 commands '
447 440 'must be declared as dicts')
448 441
449 442 def register(func):
450 443 if name in COMMANDS:
451 444 raise error.ProgrammingError('%s command already registered '
452 445 'for version 2' % name)
453 446
454 447 COMMANDS[name] = wireprototypes.commandentry(
455 448 func, args=args, transports=transports, permission=permission)
456 449
457 450 return func
458 451
459 452 return register
460 453
461 454 @wireprotocommand('branchmap', permission='pull')
462 455 def branchmapv2(repo, proto):
463 branchmap = {encoding.fromlocal(k): v
456 yield {encoding.fromlocal(k): v
464 457 for k, v in repo.branchmap().iteritems()}
465 458
466 return wireprototypes.cborresponse(branchmap)
467
468 459 @wireprotocommand('capabilities', permission='pull')
469 460 def capabilitiesv2(repo, proto):
470 caps = _capabilitiesv2(repo, proto)
471
472 return wireprototypes.cborresponse(caps)
461 yield _capabilitiesv2(repo, proto)
473 462
474 463 @wireprotocommand('heads',
475 464 args={
476 465 'publiconly': False,
477 466 },
478 467 permission='pull')
479 468 def headsv2(repo, proto, publiconly=False):
480 469 if publiconly:
481 470 repo = repo.filtered('immutable')
482 471
483 return wireprototypes.cborresponse(repo.heads())
472 yield repo.heads()
484 473
485 474 @wireprotocommand('known',
486 475 args={
487 476 'nodes': [b'deadbeef'],
488 477 },
489 478 permission='pull')
490 479 def knownv2(repo, proto, nodes=None):
491 480 nodes = nodes or []
492 481 result = b''.join(b'1' if n else b'0' for n in repo.known(nodes))
493 return wireprototypes.cborresponse(result)
482 yield result
494 483
495 484 @wireprotocommand('listkeys',
496 485 args={
497 486 'namespace': b'ns',
498 487 },
499 488 permission='pull')
500 489 def listkeysv2(repo, proto, namespace=None):
501 490 keys = repo.listkeys(encoding.tolocal(namespace))
502 491 keys = {encoding.fromlocal(k): encoding.fromlocal(v)
503 492 for k, v in keys.iteritems()}
504 493
505 return wireprototypes.cborresponse(keys)
494 yield keys
506 495
507 496 @wireprotocommand('lookup',
508 497 args={
509 498 'key': b'foo',
510 499 },
511 500 permission='pull')
512 501 def lookupv2(repo, proto, key):
513 502 key = encoding.tolocal(key)
514 503
515 504 # TODO handle exception.
516 505 node = repo.lookup(key)
517 506
518 return wireprototypes.cborresponse(node)
507 yield node
519 508
520 509 @wireprotocommand('pushkey',
521 510 args={
522 511 'namespace': b'ns',
523 512 'key': b'key',
524 513 'old': b'old',
525 514 'new': b'new',
526 515 },
527 516 permission='push')
528 517 def pushkeyv2(repo, proto, namespace, key, old, new):
529 518 # TODO handle ui output redirection
530 r = repo.pushkey(encoding.tolocal(namespace),
519 yield repo.pushkey(encoding.tolocal(namespace),
531 520 encoding.tolocal(key),
532 521 encoding.tolocal(old),
533 522 encoding.tolocal(new))
534
535 return wireprototypes.cborresponse(r)
@@ -1,571 +1,622
1 1 #require no-chg
2 2
3 3 $ . $TESTDIR/wireprotohelpers.sh
4 4 $ enabledummycommands
5 5
6 6 $ hg init server
7 7 $ cat > server/.hg/hgrc << EOF
8 8 > [experimental]
9 9 > web.apiserver = true
10 10 > EOF
11 11 $ hg -R server serve -p $HGPORT -d --pid-file hg.pid
12 12 $ cat hg.pid > $DAEMON_PIDS
13 13
14 14 HTTP v2 protocol not enabled by default
15 15
16 16 $ sendhttpraw << EOF
17 17 > httprequest GET api/$HTTPV2
18 18 > user-agent: test
19 19 > EOF
20 20 using raw connection to peer
21 21 s> GET /api/exp-http-v2-0001 HTTP/1.1\r\n
22 22 s> Accept-Encoding: identity\r\n
23 23 s> user-agent: test\r\n
24 24 s> host: $LOCALIP:$HGPORT\r\n (glob)
25 25 s> \r\n
26 26 s> makefile('rb', None)
27 27 s> HTTP/1.1 404 Not Found\r\n
28 28 s> Server: testing stub value\r\n
29 29 s> Date: $HTTP_DATE$\r\n
30 30 s> Content-Type: text/plain\r\n
31 31 s> Content-Length: 33\r\n
32 32 s> \r\n
33 33 s> API exp-http-v2-0001 not enabled\n
34 34
35 35 Restart server with support for HTTP v2 API
36 36
37 37 $ killdaemons.py
38 38 $ enablehttpv2 server
39 39 $ hg -R server serve -p $HGPORT -d --pid-file hg.pid
40 40 $ cat hg.pid > $DAEMON_PIDS
41 41
42 42 Request to unknown command yields 404
43 43
44 44 $ sendhttpraw << EOF
45 45 > httprequest POST api/$HTTPV2/ro/badcommand
46 46 > user-agent: test
47 47 > EOF
48 48 using raw connection to peer
49 49 s> POST /api/exp-http-v2-0001/ro/badcommand HTTP/1.1\r\n
50 50 s> Accept-Encoding: identity\r\n
51 51 s> user-agent: test\r\n
52 52 s> host: $LOCALIP:$HGPORT\r\n (glob)
53 53 s> \r\n
54 54 s> makefile('rb', None)
55 55 s> HTTP/1.1 404 Not Found\r\n
56 56 s> Server: testing stub value\r\n
57 57 s> Date: $HTTP_DATE$\r\n
58 58 s> Content-Type: text/plain\r\n
59 59 s> Content-Length: 42\r\n
60 60 s> \r\n
61 61 s> unknown wire protocol command: badcommand\n
62 62
63 63 GET to read-only command yields a 405
64 64
65 65 $ sendhttpraw << EOF
66 66 > httprequest GET api/$HTTPV2/ro/customreadonly
67 67 > user-agent: test
68 68 > EOF
69 69 using raw connection to peer
70 70 s> GET /api/exp-http-v2-0001/ro/customreadonly HTTP/1.1\r\n
71 71 s> Accept-Encoding: identity\r\n
72 72 s> user-agent: test\r\n
73 73 s> host: $LOCALIP:$HGPORT\r\n (glob)
74 74 s> \r\n
75 75 s> makefile('rb', None)
76 76 s> HTTP/1.1 405 Method Not Allowed\r\n
77 77 s> Server: testing stub value\r\n
78 78 s> Date: $HTTP_DATE$\r\n
79 79 s> Allow: POST\r\n
80 80 s> Content-Length: 30\r\n
81 81 s> \r\n
82 82 s> commands require POST requests
83 83
84 84 Missing Accept header results in 406
85 85
86 86 $ sendhttpraw << EOF
87 87 > httprequest POST api/$HTTPV2/ro/customreadonly
88 88 > user-agent: test
89 89 > EOF
90 90 using raw connection to peer
91 91 s> POST /api/exp-http-v2-0001/ro/customreadonly HTTP/1.1\r\n
92 92 s> Accept-Encoding: identity\r\n
93 93 s> user-agent: test\r\n
94 94 s> host: $LOCALIP:$HGPORT\r\n (glob)
95 95 s> \r\n
96 96 s> makefile('rb', None)
97 97 s> HTTP/1.1 406 Not Acceptable\r\n
98 98 s> Server: testing stub value\r\n
99 99 s> Date: $HTTP_DATE$\r\n
100 100 s> Content-Type: text/plain\r\n
101 101 s> Content-Length: 85\r\n
102 102 s> \r\n
103 103 s> client MUST specify Accept header with value: application/mercurial-exp-framing-0005\n
104 104
105 105 Bad Accept header results in 406
106 106
107 107 $ sendhttpraw << EOF
108 108 > httprequest POST api/$HTTPV2/ro/customreadonly
109 109 > accept: invalid
110 110 > user-agent: test
111 111 > EOF
112 112 using raw connection to peer
113 113 s> POST /api/exp-http-v2-0001/ro/customreadonly HTTP/1.1\r\n
114 114 s> Accept-Encoding: identity\r\n
115 115 s> accept: invalid\r\n
116 116 s> user-agent: test\r\n
117 117 s> host: $LOCALIP:$HGPORT\r\n (glob)
118 118 s> \r\n
119 119 s> makefile('rb', None)
120 120 s> HTTP/1.1 406 Not Acceptable\r\n
121 121 s> Server: testing stub value\r\n
122 122 s> Date: $HTTP_DATE$\r\n
123 123 s> Content-Type: text/plain\r\n
124 124 s> Content-Length: 85\r\n
125 125 s> \r\n
126 126 s> client MUST specify Accept header with value: application/mercurial-exp-framing-0005\n
127 127
128 128 Bad Content-Type header results in 415
129 129
130 130 $ sendhttpraw << EOF
131 131 > httprequest POST api/$HTTPV2/ro/customreadonly
132 132 > accept: $MEDIATYPE
133 133 > user-agent: test
134 134 > content-type: badmedia
135 135 > EOF
136 136 using raw connection to peer
137 137 s> POST /api/exp-http-v2-0001/ro/customreadonly HTTP/1.1\r\n
138 138 s> Accept-Encoding: identity\r\n
139 139 s> accept: application/mercurial-exp-framing-0005\r\n
140 140 s> content-type: badmedia\r\n
141 141 s> user-agent: test\r\n
142 142 s> host: $LOCALIP:$HGPORT\r\n (glob)
143 143 s> \r\n
144 144 s> makefile('rb', None)
145 145 s> HTTP/1.1 415 Unsupported Media Type\r\n
146 146 s> Server: testing stub value\r\n
147 147 s> Date: $HTTP_DATE$\r\n
148 148 s> Content-Type: text/plain\r\n
149 149 s> Content-Length: 88\r\n
150 150 s> \r\n
151 151 s> client MUST send Content-Type header with value: application/mercurial-exp-framing-0005\n
152 152
153 153 Request to read-only command works out of the box
154 154
155 155 $ sendhttpraw << EOF
156 156 > httprequest POST api/$HTTPV2/ro/customreadonly
157 157 > accept: $MEDIATYPE
158 158 > content-type: $MEDIATYPE
159 159 > user-agent: test
160 160 > frame 1 1 stream-begin command-request new cbor:{b'name': b'customreadonly'}
161 161 > EOF
162 162 using raw connection to peer
163 163 s> POST /api/exp-http-v2-0001/ro/customreadonly HTTP/1.1\r\n
164 164 s> Accept-Encoding: identity\r\n
165 165 s> *\r\n (glob)
166 166 s> content-type: application/mercurial-exp-framing-0005\r\n
167 167 s> user-agent: test\r\n
168 168 s> content-length: 29\r\n
169 169 s> host: $LOCALIP:$HGPORT\r\n (glob)
170 170 s> \r\n
171 171 s> \x15\x00\x00\x01\x00\x01\x01\x11\xa1DnameNcustomreadonly
172 172 s> makefile('rb', None)
173 173 s> HTTP/1.1 200 OK\r\n
174 174 s> Server: testing stub value\r\n
175 175 s> Date: $HTTP_DATE$\r\n
176 176 s> Content-Type: application/mercurial-exp-framing-0005\r\n
177 177 s> Transfer-Encoding: chunked\r\n
178 178 s> \r\n
179 s> 32\r\n
180 s> *\x00\x00\x01\x00\x02\x012\xa1FstatusBokX\x1dcustomreadonly bytes response
179 s> 13\r\n
180 s> \x0b\x00\x00\x01\x00\x02\x011\xa1FstatusBok
181 s> \r\n
182 s> 27\r\n
183 s> \x1f\x00\x00\x01\x00\x02\x001X\x1dcustomreadonly bytes response
184 s> \r\n
185 s> 8\r\n
186 s> \x00\x00\x00\x01\x00\x02\x002
181 187 s> \r\n
182 188 s> 0\r\n
183 189 s> \r\n
184 190
185 191 $ sendhttpv2peer << EOF
186 192 > command customreadonly
187 193 > EOF
188 194 creating http peer for wire protocol version 2
189 195 sending customreadonly command
190 196 s> POST /api/exp-http-v2-0001/ro/customreadonly HTTP/1.1\r\n
191 197 s> Accept-Encoding: identity\r\n
192 198 s> accept: application/mercurial-exp-framing-0005\r\n
193 199 s> content-type: application/mercurial-exp-framing-0005\r\n
194 200 s> content-length: 29\r\n
195 201 s> host: $LOCALIP:$HGPORT\r\n (glob)
196 202 s> user-agent: Mercurial debugwireproto\r\n
197 203 s> \r\n
198 204 s> \x15\x00\x00\x01\x00\x01\x01\x11\xa1DnameNcustomreadonly
199 205 s> makefile('rb', None)
200 206 s> HTTP/1.1 200 OK\r\n
201 207 s> Server: testing stub value\r\n
202 208 s> Date: $HTTP_DATE$\r\n
203 209 s> Content-Type: application/mercurial-exp-framing-0005\r\n
204 210 s> Transfer-Encoding: chunked\r\n
205 211 s> \r\n
206 s> 32\r\n
207 s> *\x00\x00\x01\x00\x02\x012
208 s> \xa1FstatusBokX\x1dcustomreadonly bytes response
212 s> 13\r\n
213 s> \x0b\x00\x00\x01\x00\x02\x011
214 s> \xa1FstatusBok
209 215 s> \r\n
210 received frame(size=42; request=1; stream=2; streamflags=stream-begin; type=command-response; flags=eos)
216 received frame(size=11; request=1; stream=2; streamflags=stream-begin; type=command-response; flags=continuation)
217 s> 27\r\n
218 s> \x1f\x00\x00\x01\x00\x02\x001
219 s> X\x1dcustomreadonly bytes response
220 s> \r\n
221 received frame(size=31; request=1; stream=2; streamflags=; type=command-response; flags=continuation)
222 s> 8\r\n
223 s> \x00\x00\x00\x01\x00\x02\x002
224 s> \r\n
211 225 s> 0\r\n
212 226 s> \r\n
227 received frame(size=0; request=1; stream=2; streamflags=; type=command-response; flags=eos)
213 228 response: [
214 229 {
215 230 b'status': b'ok'
216 231 },
217 232 b'customreadonly bytes response'
218 233 ]
219 234
220 235 Request to read-write command fails because server is read-only by default
221 236
222 237 GET to read-write request yields 405
223 238
224 239 $ sendhttpraw << EOF
225 240 > httprequest GET api/$HTTPV2/rw/customreadonly
226 241 > user-agent: test
227 242 > EOF
228 243 using raw connection to peer
229 244 s> GET /api/exp-http-v2-0001/rw/customreadonly HTTP/1.1\r\n
230 245 s> Accept-Encoding: identity\r\n
231 246 s> user-agent: test\r\n
232 247 s> host: $LOCALIP:$HGPORT\r\n (glob)
233 248 s> \r\n
234 249 s> makefile('rb', None)
235 250 s> HTTP/1.1 405 Method Not Allowed\r\n
236 251 s> Server: testing stub value\r\n
237 252 s> Date: $HTTP_DATE$\r\n
238 253 s> Allow: POST\r\n
239 254 s> Content-Length: 30\r\n
240 255 s> \r\n
241 256 s> commands require POST requests
242 257
243 258 Even for unknown commands
244 259
245 260 $ sendhttpraw << EOF
246 261 > httprequest GET api/$HTTPV2/rw/badcommand
247 262 > user-agent: test
248 263 > EOF
249 264 using raw connection to peer
250 265 s> GET /api/exp-http-v2-0001/rw/badcommand HTTP/1.1\r\n
251 266 s> Accept-Encoding: identity\r\n
252 267 s> user-agent: test\r\n
253 268 s> host: $LOCALIP:$HGPORT\r\n (glob)
254 269 s> \r\n
255 270 s> makefile('rb', None)
256 271 s> HTTP/1.1 405 Method Not Allowed\r\n
257 272 s> Server: testing stub value\r\n
258 273 s> Date: $HTTP_DATE$\r\n
259 274 s> Allow: POST\r\n
260 275 s> Content-Length: 30\r\n
261 276 s> \r\n
262 277 s> commands require POST requests
263 278
264 279 SSL required by default
265 280
266 281 $ sendhttpraw << EOF
267 282 > httprequest POST api/$HTTPV2/rw/customreadonly
268 283 > user-agent: test
269 284 > EOF
270 285 using raw connection to peer
271 286 s> POST /api/exp-http-v2-0001/rw/customreadonly HTTP/1.1\r\n
272 287 s> Accept-Encoding: identity\r\n
273 288 s> user-agent: test\r\n
274 289 s> host: $LOCALIP:$HGPORT\r\n (glob)
275 290 s> \r\n
276 291 s> makefile('rb', None)
277 292 s> HTTP/1.1 403 ssl required\r\n
278 293 s> Server: testing stub value\r\n
279 294 s> Date: $HTTP_DATE$\r\n
280 295 s> Content-Length: 17\r\n
281 296 s> \r\n
282 297 s> permission denied
283 298
284 299 Restart server to allow non-ssl read-write operations
285 300
286 301 $ killdaemons.py
287 302 $ cat > server/.hg/hgrc << EOF
288 303 > [experimental]
289 304 > web.apiserver = true
290 305 > web.api.http-v2 = true
291 306 > [web]
292 307 > push_ssl = false
293 308 > allow-push = *
294 309 > EOF
295 310
296 311 $ hg -R server serve -p $HGPORT -d --pid-file hg.pid -E error.log
297 312 $ cat hg.pid > $DAEMON_PIDS
298 313
299 314 Authorized request for valid read-write command works
300 315
301 316 $ sendhttpraw << EOF
302 317 > httprequest POST api/$HTTPV2/rw/customreadonly
303 318 > user-agent: test
304 319 > accept: $MEDIATYPE
305 320 > content-type: $MEDIATYPE
306 321 > frame 1 1 stream-begin command-request new cbor:{b'name': b'customreadonly'}
307 322 > EOF
308 323 using raw connection to peer
309 324 s> POST /api/exp-http-v2-0001/rw/customreadonly HTTP/1.1\r\n
310 325 s> Accept-Encoding: identity\r\n
311 326 s> accept: application/mercurial-exp-framing-0005\r\n
312 327 s> content-type: application/mercurial-exp-framing-0005\r\n
313 328 s> user-agent: test\r\n
314 329 s> content-length: 29\r\n
315 330 s> host: $LOCALIP:$HGPORT\r\n (glob)
316 331 s> \r\n
317 332 s> \x15\x00\x00\x01\x00\x01\x01\x11\xa1DnameNcustomreadonly
318 333 s> makefile('rb', None)
319 334 s> HTTP/1.1 200 OK\r\n
320 335 s> Server: testing stub value\r\n
321 336 s> Date: $HTTP_DATE$\r\n
322 337 s> Content-Type: application/mercurial-exp-framing-0005\r\n
323 338 s> Transfer-Encoding: chunked\r\n
324 339 s> \r\n
325 s> 32\r\n
326 s> *\x00\x00\x01\x00\x02\x012\xa1FstatusBokX\x1dcustomreadonly bytes response
340 s> 13\r\n
341 s> \x0b\x00\x00\x01\x00\x02\x011\xa1FstatusBok
342 s> \r\n
343 s> 27\r\n
344 s> \x1f\x00\x00\x01\x00\x02\x001X\x1dcustomreadonly bytes response
345 s> \r\n
346 s> 8\r\n
347 s> \x00\x00\x00\x01\x00\x02\x002
327 348 s> \r\n
328 349 s> 0\r\n
329 350 s> \r\n
330 351
331 352 Authorized request for unknown command is rejected
332 353
333 354 $ sendhttpraw << EOF
334 355 > httprequest POST api/$HTTPV2/rw/badcommand
335 356 > user-agent: test
336 357 > accept: $MEDIATYPE
337 358 > EOF
338 359 using raw connection to peer
339 360 s> POST /api/exp-http-v2-0001/rw/badcommand HTTP/1.1\r\n
340 361 s> Accept-Encoding: identity\r\n
341 362 s> accept: application/mercurial-exp-framing-0005\r\n
342 363 s> user-agent: test\r\n
343 364 s> host: $LOCALIP:$HGPORT\r\n (glob)
344 365 s> \r\n
345 366 s> makefile('rb', None)
346 367 s> HTTP/1.1 404 Not Found\r\n
347 368 s> Server: testing stub value\r\n
348 369 s> Date: $HTTP_DATE$\r\n
349 370 s> Content-Type: text/plain\r\n
350 371 s> Content-Length: 42\r\n
351 372 s> \r\n
352 373 s> unknown wire protocol command: badcommand\n
353 374
354 375 debugreflect isn't enabled by default
355 376
356 377 $ sendhttpraw << EOF
357 378 > httprequest POST api/$HTTPV2/ro/debugreflect
358 379 > user-agent: test
359 380 > EOF
360 381 using raw connection to peer
361 382 s> POST /api/exp-http-v2-0001/ro/debugreflect HTTP/1.1\r\n
362 383 s> Accept-Encoding: identity\r\n
363 384 s> user-agent: test\r\n
364 385 s> host: $LOCALIP:$HGPORT\r\n (glob)
365 386 s> \r\n
366 387 s> makefile('rb', None)
367 388 s> HTTP/1.1 404 Not Found\r\n
368 389 s> Server: testing stub value\r\n
369 390 s> Date: $HTTP_DATE$\r\n
370 391 s> Content-Type: text/plain\r\n
371 392 s> Content-Length: 34\r\n
372 393 s> \r\n
373 394 s> debugreflect service not available
374 395
375 396 Restart server to get debugreflect endpoint
376 397
377 398 $ killdaemons.py
378 399 $ cat > server/.hg/hgrc << EOF
379 400 > [experimental]
380 401 > web.apiserver = true
381 402 > web.api.debugreflect = true
382 403 > web.api.http-v2 = true
383 404 > [web]
384 405 > push_ssl = false
385 406 > allow-push = *
386 407 > EOF
387 408
388 409 $ hg -R server serve -p $HGPORT -d --pid-file hg.pid -E error.log
389 410 $ cat hg.pid > $DAEMON_PIDS
390 411
391 412 Command frames can be reflected via debugreflect
392 413
393 414 $ sendhttpraw << EOF
394 415 > httprequest POST api/$HTTPV2/ro/debugreflect
395 416 > accept: $MEDIATYPE
396 417 > content-type: $MEDIATYPE
397 418 > user-agent: test
398 419 > frame 1 1 stream-begin command-request new cbor:{b'name': b'command1', b'args': {b'foo': b'val1', b'bar1': b'val'}}
399 420 > EOF
400 421 using raw connection to peer
401 422 s> POST /api/exp-http-v2-0001/ro/debugreflect HTTP/1.1\r\n
402 423 s> Accept-Encoding: identity\r\n
403 424 s> accept: application/mercurial-exp-framing-0005\r\n
404 425 s> content-type: application/mercurial-exp-framing-0005\r\n
405 426 s> user-agent: test\r\n
406 427 s> content-length: 47\r\n
407 428 s> host: $LOCALIP:$HGPORT\r\n (glob)
408 429 s> \r\n
409 430 s> \'\x00\x00\x01\x00\x01\x01\x11\xa2Dargs\xa2Dbar1CvalCfooDval1DnameHcommand1
410 431 s> makefile('rb', None)
411 432 s> HTTP/1.1 200 OK\r\n
412 433 s> Server: testing stub value\r\n
413 434 s> Date: $HTTP_DATE$\r\n
414 435 s> Content-Type: text/plain\r\n
415 436 s> Content-Length: 205\r\n
416 437 s> \r\n
417 438 s> received: 1 1 1 \xa2Dargs\xa2Dbar1CvalCfooDval1DnameHcommand1\n
418 439 s> ["runcommand", {"args": {"bar1": "val", "foo": "val1"}, "command": "command1", "data": null, "requestid": 1}]\n
419 440 s> received: <no frame>\n
420 441 s> {"action": "noop"}
421 442
422 443 Multiple requests to regular command URL are not allowed
423 444
424 445 $ sendhttpraw << EOF
425 446 > httprequest POST api/$HTTPV2/ro/customreadonly
426 447 > accept: $MEDIATYPE
427 448 > content-type: $MEDIATYPE
428 449 > user-agent: test
429 450 > frame 1 1 stream-begin command-request new cbor:{b'name': b'customreadonly'}
430 451 > EOF
431 452 using raw connection to peer
432 453 s> POST /api/exp-http-v2-0001/ro/customreadonly HTTP/1.1\r\n
433 454 s> Accept-Encoding: identity\r\n
434 455 s> accept: application/mercurial-exp-framing-0005\r\n
435 456 s> content-type: application/mercurial-exp-framing-0005\r\n
436 457 s> user-agent: test\r\n
437 458 s> content-length: 29\r\n
438 459 s> host: $LOCALIP:$HGPORT\r\n (glob)
439 460 s> \r\n
440 461 s> \x15\x00\x00\x01\x00\x01\x01\x11\xa1DnameNcustomreadonly
441 462 s> makefile('rb', None)
442 463 s> HTTP/1.1 200 OK\r\n
443 464 s> Server: testing stub value\r\n
444 465 s> Date: $HTTP_DATE$\r\n
445 466 s> Content-Type: application/mercurial-exp-framing-0005\r\n
446 467 s> Transfer-Encoding: chunked\r\n
447 468 s> \r\n
448 s> 32\r\n
449 s> *\x00\x00\x01\x00\x02\x012\xa1FstatusBokX\x1dcustomreadonly bytes response
469 s> 13\r\n
470 s> \x0b\x00\x00\x01\x00\x02\x011\xa1FstatusBok
471 s> \r\n
472 s> 27\r\n
473 s> \x1f\x00\x00\x01\x00\x02\x001X\x1dcustomreadonly bytes response
474 s> \r\n
475 s> 8\r\n
476 s> \x00\x00\x00\x01\x00\x02\x002
450 477 s> \r\n
451 478 s> 0\r\n
452 479 s> \r\n
453 480
454 481 Multiple requests to "multirequest" URL are allowed
455 482
456 483 $ sendhttpraw << EOF
457 484 > httprequest POST api/$HTTPV2/ro/multirequest
458 485 > accept: $MEDIATYPE
459 486 > content-type: $MEDIATYPE
460 487 > user-agent: test
461 488 > frame 1 1 stream-begin command-request new cbor:{b'name': b'customreadonly'}
462 489 > frame 3 1 0 command-request new cbor:{b'name': b'customreadonly'}
463 490 > EOF
464 491 using raw connection to peer
465 492 s> POST /api/exp-http-v2-0001/ro/multirequest HTTP/1.1\r\n
466 493 s> Accept-Encoding: identity\r\n
467 494 s> *\r\n (glob)
468 495 s> *\r\n (glob)
469 496 s> user-agent: test\r\n
470 497 s> content-length: 58\r\n
471 498 s> host: $LOCALIP:$HGPORT\r\n (glob)
472 499 s> \r\n
473 500 s> \x15\x00\x00\x01\x00\x01\x01\x11\xa1DnameNcustomreadonly\x15\x00\x00\x03\x00\x01\x00\x11\xa1DnameNcustomreadonly
474 501 s> makefile('rb', None)
475 502 s> HTTP/1.1 200 OK\r\n
476 503 s> Server: testing stub value\r\n
477 504 s> Date: $HTTP_DATE$\r\n
478 505 s> Content-Type: application/mercurial-exp-framing-0005\r\n
479 506 s> Transfer-Encoding: chunked\r\n
480 507 s> \r\n
481 s> 32\r\n
482 s> *\x00\x00\x01\x00\x02\x012\xa1FstatusBokX\x1dcustomreadonly bytes response
508 s> 13\r\n
509 s> \x0b\x00\x00\x01\x00\x02\x011\xa1FstatusBok
510 s> \r\n
511 s> 27\r\n
512 s> \x1f\x00\x00\x01\x00\x02\x001X\x1dcustomreadonly bytes response
513 s> \r\n
514 s> 8\r\n
515 s> \x00\x00\x00\x01\x00\x02\x002
483 516 s> \r\n
484 s> 32\r\n
485 s> *\x00\x00\x03\x00\x02\x002\xa1FstatusBokX\x1dcustomreadonly bytes response
517 s> 13\r\n
518 s> \x0b\x00\x00\x03\x00\x02\x001\xa1FstatusBok
519 s> \r\n
520 s> 27\r\n
521 s> \x1f\x00\x00\x03\x00\x02\x001X\x1dcustomreadonly bytes response
522 s> \r\n
523 s> 8\r\n
524 s> \x00\x00\x00\x03\x00\x02\x002
486 525 s> \r\n
487 526 s> 0\r\n
488 527 s> \r\n
489 528
490 529 Interleaved requests to "multirequest" are processed
491 530
492 531 $ sendhttpraw << EOF
493 532 > httprequest POST api/$HTTPV2/ro/multirequest
494 533 > accept: $MEDIATYPE
495 534 > content-type: $MEDIATYPE
496 535 > user-agent: test
497 536 > frame 1 1 stream-begin command-request new|more \xa2Dargs\xa1Inamespace
498 537 > frame 3 1 0 command-request new|more \xa2Dargs\xa1Inamespace
499 538 > frame 3 1 0 command-request continuation JnamespacesDnameHlistkeys
500 539 > frame 1 1 0 command-request continuation IbookmarksDnameHlistkeys
501 540 > EOF
502 541 using raw connection to peer
503 542 s> POST /api/exp-http-v2-0001/ro/multirequest HTTP/1.1\r\n
504 543 s> Accept-Encoding: identity\r\n
505 544 s> accept: application/mercurial-exp-framing-0005\r\n
506 545 s> content-type: application/mercurial-exp-framing-0005\r\n
507 546 s> user-agent: test\r\n
508 547 s> content-length: 115\r\n
509 548 s> host: $LOCALIP:$HGPORT\r\n (glob)
510 549 s> \r\n
511 550 s> \x11\x00\x00\x01\x00\x01\x01\x15\xa2Dargs\xa1Inamespace\x11\x00\x00\x03\x00\x01\x00\x15\xa2Dargs\xa1Inamespace\x19\x00\x00\x03\x00\x01\x00\x12JnamespacesDnameHlistkeys\x18\x00\x00\x01\x00\x01\x00\x12IbookmarksDnameHlistkeys
512 551 s> makefile('rb', None)
513 552 s> HTTP/1.1 200 OK\r\n
514 553 s> Server: testing stub value\r\n
515 554 s> Date: $HTTP_DATE$\r\n
516 555 s> Content-Type: application/mercurial-exp-framing-0005\r\n
517 556 s> Transfer-Encoding: chunked\r\n
518 557 s> \r\n
519 s> 33\r\n
520 s> +\x00\x00\x03\x00\x02\x012\xa1FstatusBok\xa3Ibookmarks@Jnamespaces@Fphases@
558 s> 13\r\n
559 s> \x0b\x00\x00\x03\x00\x02\x011\xa1FstatusBok
560 s> \r\n
561 s> 28\r\n
562 s> \x00\x00\x03\x00\x02\x001\xa3Ibookmarks@Jnamespaces@Fphases@
563 s> \r\n
564 s> 8\r\n
565 s> \x00\x00\x00\x03\x00\x02\x002
521 566 s> \r\n
522 s> 14\r\n
523 s> \x0c\x00\x00\x01\x00\x02\x002\xa1FstatusBok\xa0
567 s> 13\r\n
568 s> \x0b\x00\x00\x01\x00\x02\x001\xa1FstatusBok
569 s> \r\n
570 s> 9\r\n
571 s> \x01\x00\x00\x01\x00\x02\x001\xa0
572 s> \r\n
573 s> 8\r\n
574 s> \x00\x00\x00\x01\x00\x02\x002
524 575 s> \r\n
525 576 s> 0\r\n
526 577 s> \r\n
527 578
528 579 Restart server to disable read-write access
529 580
530 581 $ killdaemons.py
531 582 $ cat > server/.hg/hgrc << EOF
532 583 > [experimental]
533 584 > web.apiserver = true
534 585 > web.api.debugreflect = true
535 586 > web.api.http-v2 = true
536 587 > [web]
537 588 > push_ssl = false
538 589 > EOF
539 590
540 591 $ hg -R server serve -p $HGPORT -d --pid-file hg.pid -E error.log
541 592 $ cat hg.pid > $DAEMON_PIDS
542 593
543 594 Attempting to run a read-write command via multirequest on read-only URL is not allowed
544 595
545 596 $ sendhttpraw << EOF
546 597 > httprequest POST api/$HTTPV2/ro/multirequest
547 598 > accept: $MEDIATYPE
548 599 > content-type: $MEDIATYPE
549 600 > user-agent: test
550 601 > frame 1 1 stream-begin command-request new cbor:{b'name': b'pushkey'}
551 602 > EOF
552 603 using raw connection to peer
553 604 s> POST /api/exp-http-v2-0001/ro/multirequest HTTP/1.1\r\n
554 605 s> Accept-Encoding: identity\r\n
555 606 s> accept: application/mercurial-exp-framing-0005\r\n
556 607 s> content-type: application/mercurial-exp-framing-0005\r\n
557 608 s> user-agent: test\r\n
558 609 s> content-length: 22\r\n
559 610 s> host: $LOCALIP:$HGPORT\r\n (glob)
560 611 s> \r\n
561 612 s> \x0e\x00\x00\x01\x00\x01\x01\x11\xa1DnameGpushkey
562 613 s> makefile('rb', None)
563 614 s> HTTP/1.1 403 Forbidden\r\n
564 615 s> Server: testing stub value\r\n
565 616 s> Date: $HTTP_DATE$\r\n
566 617 s> Content-Type: text/plain\r\n
567 618 s> Content-Length: 52\r\n
568 619 s> \r\n
569 620 s> insufficient permissions to execute command: pushkey
570 621
571 622 $ cat error.log
@@ -1,740 +1,749
1 1 #require no-chg
2 2
3 3 $ . $TESTDIR/wireprotohelpers.sh
4 4
5 5 $ cat >> $HGRCPATH << EOF
6 6 > [web]
7 7 > push_ssl = false
8 8 > allow_push = *
9 9 > EOF
10 10
11 11 $ hg init server
12 12 $ cd server
13 13 $ touch a
14 14 $ hg -q commit -A -m initial
15 15 $ cd ..
16 16
17 17 $ hg serve -R server -p $HGPORT -d --pid-file hg.pid
18 18 $ cat hg.pid >> $DAEMON_PIDS
19 19
20 20 compression formats are advertised in compression capability
21 21
22 22 #if zstd
23 23 $ get-with-headers.py $LOCALIP:$HGPORT '?cmd=capabilities' | tr ' ' '\n' | grep '^compression=zstd,zlib$' > /dev/null
24 24 #else
25 25 $ get-with-headers.py $LOCALIP:$HGPORT '?cmd=capabilities' | tr ' ' '\n' | grep '^compression=zlib$' > /dev/null
26 26 #endif
27 27
28 28 $ killdaemons.py
29 29
30 30 server.compressionengines can replace engines list wholesale
31 31
32 32 $ hg serve --config server.compressionengines=none -R server -p $HGPORT -d --pid-file hg.pid
33 33 $ cat hg.pid > $DAEMON_PIDS
34 34 $ get-with-headers.py $LOCALIP:$HGPORT '?cmd=capabilities' | tr ' ' '\n' | grep '^compression=none$' > /dev/null
35 35
36 36 $ killdaemons.py
37 37
38 38 Order of engines can also change
39 39
40 40 $ hg serve --config server.compressionengines=none,zlib -R server -p $HGPORT -d --pid-file hg.pid
41 41 $ cat hg.pid > $DAEMON_PIDS
42 42 $ get-with-headers.py $LOCALIP:$HGPORT '?cmd=capabilities' | tr ' ' '\n' | grep '^compression=none,zlib$' > /dev/null
43 43
44 44 $ killdaemons.py
45 45
46 46 Start a default server again
47 47
48 48 $ hg serve -R server -p $HGPORT -d --pid-file hg.pid
49 49 $ cat hg.pid > $DAEMON_PIDS
50 50
51 51 Server should send application/mercurial-0.1 to clients if no Accept is used
52 52
53 53 $ get-with-headers.py --headeronly $LOCALIP:$HGPORT '?cmd=getbundle&heads=e93700bd72895c5addab234c56d4024b487a362f&common=0000000000000000000000000000000000000000' -
54 54 200 Script output follows
55 55 content-type: application/mercurial-0.1
56 56 date: $HTTP_DATE$
57 57 server: testing stub value
58 58 transfer-encoding: chunked
59 59
60 60 Server should send application/mercurial-0.1 when client says it wants it
61 61
62 62 $ get-with-headers.py --hgproto '0.1' --headeronly $LOCALIP:$HGPORT '?cmd=getbundle&heads=e93700bd72895c5addab234c56d4024b487a362f&common=0000000000000000000000000000000000000000' -
63 63 200 Script output follows
64 64 content-type: application/mercurial-0.1
65 65 date: $HTTP_DATE$
66 66 server: testing stub value
67 67 transfer-encoding: chunked
68 68
69 69 Server should send application/mercurial-0.2 when client says it wants it
70 70
71 71 $ get-with-headers.py --hgproto '0.2' --headeronly $LOCALIP:$HGPORT '?cmd=getbundle&heads=e93700bd72895c5addab234c56d4024b487a362f&common=0000000000000000000000000000000000000000' -
72 72 200 Script output follows
73 73 content-type: application/mercurial-0.2
74 74 date: $HTTP_DATE$
75 75 server: testing stub value
76 76 transfer-encoding: chunked
77 77
78 78 $ get-with-headers.py --hgproto '0.1 0.2' --headeronly $LOCALIP:$HGPORT '?cmd=getbundle&heads=e93700bd72895c5addab234c56d4024b487a362f&common=0000000000000000000000000000000000000000' -
79 79 200 Script output follows
80 80 content-type: application/mercurial-0.2
81 81 date: $HTTP_DATE$
82 82 server: testing stub value
83 83 transfer-encoding: chunked
84 84
85 85 Requesting a compression format that server doesn't support results will fall back to 0.1
86 86
87 87 $ get-with-headers.py --hgproto '0.2 comp=aa' --headeronly $LOCALIP:$HGPORT '?cmd=getbundle&heads=e93700bd72895c5addab234c56d4024b487a362f&common=0000000000000000000000000000000000000000' -
88 88 200 Script output follows
89 89 content-type: application/mercurial-0.1
90 90 date: $HTTP_DATE$
91 91 server: testing stub value
92 92 transfer-encoding: chunked
93 93
94 94 #if zstd
95 95 zstd is used if available
96 96
97 97 $ get-with-headers.py --hgproto '0.2 comp=zstd' $LOCALIP:$HGPORT '?cmd=getbundle&heads=e93700bd72895c5addab234c56d4024b487a362f&common=0000000000000000000000000000000000000000' > resp
98 98 $ f --size --hexdump --bytes 36 --sha1 resp
99 99 resp: size=248, sha1=4d8d8f87fb82bd542ce52881fdc94f850748
100 100 0000: 32 30 30 20 53 63 72 69 70 74 20 6f 75 74 70 75 |200 Script outpu|
101 101 0010: 74 20 66 6f 6c 6c 6f 77 73 0a 0a 04 7a 73 74 64 |t follows...zstd|
102 102 0020: 28 b5 2f fd |(./.|
103 103
104 104 #endif
105 105
106 106 application/mercurial-0.2 is not yet used on non-streaming responses
107 107
108 108 $ get-with-headers.py --hgproto '0.2' $LOCALIP:$HGPORT '?cmd=heads' -
109 109 200 Script output follows
110 110 content-length: 41
111 111 content-type: application/mercurial-0.1
112 112 date: $HTTP_DATE$
113 113 server: testing stub value
114 114
115 115 e93700bd72895c5addab234c56d4024b487a362f
116 116
117 117 Now test protocol preference usage
118 118
119 119 $ killdaemons.py
120 120 $ hg serve --config server.compressionengines=none,zlib -R server -p $HGPORT -d --pid-file hg.pid
121 121 $ cat hg.pid > $DAEMON_PIDS
122 122
123 123 No Accept will send 0.1+zlib, even though "none" is preferred b/c "none" isn't supported on 0.1
124 124
125 125 $ get-with-headers.py --headeronly $LOCALIP:$HGPORT '?cmd=getbundle&heads=e93700bd72895c5addab234c56d4024b487a362f&common=0000000000000000000000000000000000000000' Content-Type
126 126 200 Script output follows
127 127 content-type: application/mercurial-0.1
128 128
129 129 $ get-with-headers.py $LOCALIP:$HGPORT '?cmd=getbundle&heads=e93700bd72895c5addab234c56d4024b487a362f&common=0000000000000000000000000000000000000000' > resp
130 130 $ f --size --hexdump --bytes 28 --sha1 resp
131 131 resp: size=227, sha1=35a4c074da74f32f5440da3cbf04
132 132 0000: 32 30 30 20 53 63 72 69 70 74 20 6f 75 74 70 75 |200 Script outpu|
133 133 0010: 74 20 66 6f 6c 6c 6f 77 73 0a 0a 78 |t follows..x|
134 134
135 135 Explicit 0.1 will send zlib because "none" isn't supported on 0.1
136 136
137 137 $ get-with-headers.py --hgproto '0.1' $LOCALIP:$HGPORT '?cmd=getbundle&heads=e93700bd72895c5addab234c56d4024b487a362f&common=0000000000000000000000000000000000000000' > resp
138 138 $ f --size --hexdump --bytes 28 --sha1 resp
139 139 resp: size=227, sha1=35a4c074da74f32f5440da3cbf04
140 140 0000: 32 30 30 20 53 63 72 69 70 74 20 6f 75 74 70 75 |200 Script outpu|
141 141 0010: 74 20 66 6f 6c 6c 6f 77 73 0a 0a 78 |t follows..x|
142 142
143 143 0.2 with no compression will get "none" because that is server's preference
144 144 (spec says ZL and UN are implicitly supported)
145 145
146 146 $ get-with-headers.py --hgproto '0.2' $LOCALIP:$HGPORT '?cmd=getbundle&heads=e93700bd72895c5addab234c56d4024b487a362f&common=0000000000000000000000000000000000000000' > resp
147 147 $ f --size --hexdump --bytes 32 --sha1 resp
148 148 resp: size=432, sha1=ac931b412ec185a02e0e5bcff98dac83
149 149 0000: 32 30 30 20 53 63 72 69 70 74 20 6f 75 74 70 75 |200 Script outpu|
150 150 0010: 74 20 66 6f 6c 6c 6f 77 73 0a 0a 04 6e 6f 6e 65 |t follows...none|
151 151
152 152 Client receives server preference even if local order doesn't match
153 153
154 154 $ get-with-headers.py --hgproto '0.2 comp=zlib,none' $LOCALIP:$HGPORT '?cmd=getbundle&heads=e93700bd72895c5addab234c56d4024b487a362f&common=0000000000000000000000000000000000000000' > resp
155 155 $ f --size --hexdump --bytes 32 --sha1 resp
156 156 resp: size=432, sha1=ac931b412ec185a02e0e5bcff98dac83
157 157 0000: 32 30 30 20 53 63 72 69 70 74 20 6f 75 74 70 75 |200 Script outpu|
158 158 0010: 74 20 66 6f 6c 6c 6f 77 73 0a 0a 04 6e 6f 6e 65 |t follows...none|
159 159
160 160 Client receives only supported format even if not server preferred format
161 161
162 162 $ get-with-headers.py --hgproto '0.2 comp=zlib' $LOCALIP:$HGPORT '?cmd=getbundle&heads=e93700bd72895c5addab234c56d4024b487a362f&common=0000000000000000000000000000000000000000' > resp
163 163 $ f --size --hexdump --bytes 33 --sha1 resp
164 164 resp: size=232, sha1=a1c727f0c9693ca15742a75c30419bc36
165 165 0000: 32 30 30 20 53 63 72 69 70 74 20 6f 75 74 70 75 |200 Script outpu|
166 166 0010: 74 20 66 6f 6c 6c 6f 77 73 0a 0a 04 7a 6c 69 62 |t follows...zlib|
167 167 0020: 78 |x|
168 168
169 169 $ killdaemons.py
170 170 $ cd ..
171 171
172 172 Test listkeys for listing namespaces
173 173
174 174 $ hg init empty
175 175 $ hg -R empty serve -p $HGPORT -d --pid-file hg.pid
176 176 $ cat hg.pid > $DAEMON_PIDS
177 177
178 178 $ hg --verbose debugwireproto http://$LOCALIP:$HGPORT << EOF
179 179 > command listkeys
180 180 > namespace namespaces
181 181 > EOF
182 182 s> GET /?cmd=capabilities HTTP/1.1\r\n
183 183 s> Accept-Encoding: identity\r\n
184 184 s> accept: application/mercurial-0.1\r\n
185 185 s> host: $LOCALIP:$HGPORT\r\n (glob)
186 186 s> user-agent: Mercurial debugwireproto\r\n
187 187 s> \r\n
188 188 s> makefile('rb', None)
189 189 s> HTTP/1.1 200 Script output follows\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-0.1\r\n
193 193 s> Content-Length: *\r\n (glob)
194 194 s> \r\n
195 195 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
196 196 sending listkeys command
197 197 s> GET /?cmd=listkeys HTTP/1.1\r\n
198 198 s> Accept-Encoding: identity\r\n
199 199 s> vary: X-HgArg-1,X-HgProto-1\r\n
200 200 s> x-hgarg-1: namespace=namespaces\r\n
201 201 s> x-hgproto-1: 0.1 0.2 comp=$USUAL_COMPRESSIONS$ partial-pull\r\n
202 202 s> accept: application/mercurial-0.1\r\n
203 203 s> host: $LOCALIP:$HGPORT\r\n (glob)
204 204 s> user-agent: Mercurial debugwireproto\r\n
205 205 s> \r\n
206 206 s> makefile('rb', None)
207 207 s> HTTP/1.1 200 Script output follows\r\n
208 208 s> Server: testing stub value\r\n
209 209 s> Date: $HTTP_DATE$\r\n
210 210 s> Content-Type: application/mercurial-0.1\r\n
211 211 s> Content-Length: 30\r\n
212 212 s> \r\n
213 213 s> bookmarks\t\n
214 214 s> namespaces\t\n
215 215 s> phases\t
216 216 response: {
217 217 b'bookmarks': b'',
218 218 b'namespaces': b'',
219 219 b'phases': b''
220 220 }
221 221
222 222 Same thing, but with "httprequest" command
223 223
224 224 $ hg --verbose debugwireproto --peer raw http://$LOCALIP:$HGPORT << EOF
225 225 > httprequest GET ?cmd=listkeys
226 226 > user-agent: test
227 227 > x-hgarg-1: namespace=namespaces
228 228 > EOF
229 229 using raw connection to peer
230 230 s> GET /?cmd=listkeys HTTP/1.1\r\n
231 231 s> Accept-Encoding: identity\r\n
232 232 s> user-agent: test\r\n
233 233 s> x-hgarg-1: namespace=namespaces\r\n
234 234 s> host: $LOCALIP:$HGPORT\r\n (glob)
235 235 s> \r\n
236 236 s> makefile('rb', None)
237 237 s> HTTP/1.1 200 Script output follows\r\n
238 238 s> Server: testing stub value\r\n
239 239 s> Date: $HTTP_DATE$\r\n
240 240 s> Content-Type: application/mercurial-0.1\r\n
241 241 s> Content-Length: 30\r\n
242 242 s> \r\n
243 243 s> bookmarks\t\n
244 244 s> namespaces\t\n
245 245 s> phases\t
246 246
247 247 Client with HTTPv2 enabled advertises that and gets old capabilities response from old server
248 248
249 249 $ hg --config experimental.httppeer.advertise-v2=true --verbose debugwireproto http://$LOCALIP:$HGPORT << EOF
250 250 > command heads
251 251 > EOF
252 252 s> GET /?cmd=capabilities HTTP/1.1\r\n
253 253 s> Accept-Encoding: identity\r\n
254 254 s> vary: X-HgProto-1,X-HgUpgrade-1\r\n
255 255 s> x-hgproto-1: cbor\r\n
256 256 s> x-hgupgrade-1: exp-http-v2-0001\r\n
257 257 s> accept: application/mercurial-0.1\r\n
258 258 s> host: $LOCALIP:$HGPORT\r\n (glob)
259 259 s> user-agent: Mercurial debugwireproto\r\n
260 260 s> \r\n
261 261 s> makefile('rb', None)
262 262 s> HTTP/1.1 200 Script output follows\r\n
263 263 s> Server: testing stub value\r\n
264 264 s> Date: $HTTP_DATE$\r\n
265 265 s> Content-Type: application/mercurial-0.1\r\n
266 266 s> Content-Length: *\r\n (glob)
267 267 s> \r\n
268 268 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
269 269 sending heads command
270 270 s> GET /?cmd=heads HTTP/1.1\r\n
271 271 s> Accept-Encoding: identity\r\n
272 272 s> vary: X-HgProto-1\r\n
273 273 s> x-hgproto-1: 0.1 0.2 comp=$USUAL_COMPRESSIONS$ partial-pull\r\n
274 274 s> accept: application/mercurial-0.1\r\n
275 275 s> host: $LOCALIP:$HGPORT\r\n (glob)
276 276 s> user-agent: Mercurial debugwireproto\r\n
277 277 s> \r\n
278 278 s> makefile('rb', None)
279 279 s> HTTP/1.1 200 Script output follows\r\n
280 280 s> Server: testing stub value\r\n
281 281 s> Date: $HTTP_DATE$\r\n
282 282 s> Content-Type: application/mercurial-0.1\r\n
283 283 s> Content-Length: 41\r\n
284 284 s> \r\n
285 285 s> 0000000000000000000000000000000000000000\n
286 286 response: [
287 287 b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'
288 288 ]
289 289
290 290 $ killdaemons.py
291 291 $ enablehttpv2 empty
292 292 $ hg --config server.compressionengines=zlib -R empty serve -p $HGPORT -d --pid-file hg.pid
293 293 $ cat hg.pid > $DAEMON_PIDS
294 294
295 295 Client with HTTPv2 enabled automatically upgrades if the server supports it
296 296
297 297 $ hg --config experimental.httppeer.advertise-v2=true --verbose debugwireproto http://$LOCALIP:$HGPORT << EOF
298 298 > command heads
299 299 > EOF
300 300 s> GET /?cmd=capabilities HTTP/1.1\r\n
301 301 s> Accept-Encoding: identity\r\n
302 302 s> vary: X-HgProto-1,X-HgUpgrade-1\r\n
303 303 s> x-hgproto-1: cbor\r\n
304 304 s> x-hgupgrade-1: exp-http-v2-0001\r\n
305 305 s> accept: application/mercurial-0.1\r\n
306 306 s> host: $LOCALIP:$HGPORT\r\n (glob)
307 307 s> user-agent: Mercurial debugwireproto\r\n
308 308 s> \r\n
309 309 s> makefile('rb', None)
310 310 s> HTTP/1.1 200 OK\r\n
311 311 s> Server: testing stub value\r\n
312 312 s> Date: $HTTP_DATE$\r\n
313 313 s> Content-Type: application/mercurial-cbor\r\n
314 314 s> Content-Length: *\r\n (glob)
315 315 s> \r\n
316 316 s> \xa3GapibaseDapi/Dapis\xa1Pexp-http-v2-0001\xa4Hcommands\xa7Ibranchmap\xa2Dargs\xa0Kpermissions\x81DpullLcapabilities\xa2Dargs\xa0Kpermissions\x81DpullEheads\xa2Dargs\xa1Jpubliconly\xf4Kpermissions\x81DpullEknown\xa2Dargs\xa1Enodes\x81HdeadbeefKpermissions\x81DpullHlistkeys\xa2Dargs\xa1InamespaceBnsKpermissions\x81DpullFlookup\xa2Dargs\xa1CkeyCfooKpermissions\x81DpullGpushkey\xa2Dargs\xa4CkeyCkeyInamespaceBnsCnewCnewColdColdKpermissions\x81DpushKcompression\x81\xa1DnameDzlibQframingmediatypes\x81X&application/mercurial-exp-framing-0005Nrawrepoformats\x82LgeneraldeltaHrevlogv1Nv1capabilitiesY\x01\xc5batch 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
317 317 sending heads command
318 318 s> POST /api/exp-http-v2-0001/ro/heads HTTP/1.1\r\n
319 319 s> Accept-Encoding: identity\r\n
320 320 s> accept: application/mercurial-exp-framing-0005\r\n
321 321 s> content-type: application/mercurial-exp-framing-0005\r\n
322 322 s> content-length: 20\r\n
323 323 s> host: $LOCALIP:$HGPORT\r\n (glob)
324 324 s> user-agent: Mercurial debugwireproto\r\n
325 325 s> \r\n
326 326 s> \x0c\x00\x00\x01\x00\x01\x01\x11\xa1DnameEheads
327 327 s> makefile('rb', None)
328 328 s> HTTP/1.1 200 OK\r\n
329 329 s> Server: testing stub value\r\n
330 330 s> Date: $HTTP_DATE$\r\n
331 331 s> Content-Type: application/mercurial-exp-framing-0005\r\n
332 332 s> Transfer-Encoding: chunked\r\n
333 333 s> \r\n
334 s> 29\r\n
335 s> !\x00\x00\x01\x00\x02\x012
336 s> \xa1FstatusBok\x81T\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00
334 s> 13\r\n
335 s> \x0b\x00\x00\x01\x00\x02\x011
336 s> \xa1FstatusBok
337 337 s> \r\n
338 received frame(size=33; request=1; stream=2; streamflags=stream-begin; type=command-response; flags=eos)
338 received frame(size=11; request=1; stream=2; streamflags=stream-begin; type=command-response; flags=continuation)
339 s> 1e\r\n
340 s> \x16\x00\x00\x01\x00\x02\x001
341 s> \x81T\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00
342 s> \r\n
343 received frame(size=22; request=1; stream=2; streamflags=; type=command-response; flags=continuation)
344 s> 8\r\n
345 s> \x00\x00\x00\x01\x00\x02\x002
346 s> \r\n
339 347 s> 0\r\n
340 348 s> \r\n
349 received frame(size=0; request=1; stream=2; streamflags=; type=command-response; flags=eos)
341 350 response: [
342 351 b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'
343 352 ]
344 353
345 354 $ killdaemons.py
346 355
347 356 HTTP client follows HTTP redirect on handshake to new repo
348 357
349 358 $ cd $TESTTMP
350 359
351 360 $ hg init redirector
352 361 $ hg init redirected
353 362 $ cd redirected
354 363 $ touch foo
355 364 $ hg -q commit -A -m initial
356 365 $ cd ..
357 366
358 367 $ cat > paths.conf << EOF
359 368 > [paths]
360 369 > / = $TESTTMP/*
361 370 > EOF
362 371
363 372 $ cat > redirectext.py << EOF
364 373 > from mercurial import extensions, wireprotoserver
365 374 > def wrappedcallhttp(orig, repo, req, res, proto, cmd):
366 375 > path = req.advertisedurl[len(req.advertisedbaseurl):]
367 376 > if not path.startswith(b'/redirector'):
368 377 > return orig(repo, req, res, proto, cmd)
369 378 > relpath = path[len(b'/redirector'):]
370 379 > res.status = b'301 Redirect'
371 380 > newurl = b'%s/redirected%s' % (req.baseurl, relpath)
372 381 > if not repo.ui.configbool('testing', 'redirectqs', True) and b'?' in newurl:
373 382 > newurl = newurl[0:newurl.index(b'?')]
374 383 > res.headers[b'Location'] = newurl
375 384 > res.headers[b'Content-Type'] = b'text/plain'
376 385 > res.setbodybytes(b'redirected')
377 386 > return True
378 387 >
379 388 > extensions.wrapfunction(wireprotoserver, '_callhttp', wrappedcallhttp)
380 389 > EOF
381 390
382 391 $ hg --config extensions.redirect=$TESTTMP/redirectext.py \
383 392 > --config server.compressionengines=zlib \
384 393 > serve --web-conf paths.conf --pid-file hg.pid -p $HGPORT -d
385 394 $ cat hg.pid > $DAEMON_PIDS
386 395
387 396 Verify our HTTP 301 is served properly
388 397
389 398 $ hg --verbose debugwireproto --peer raw http://$LOCALIP:$HGPORT << EOF
390 399 > httprequest GET /redirector?cmd=capabilities
391 400 > user-agent: test
392 401 > EOF
393 402 using raw connection to peer
394 403 s> GET /redirector?cmd=capabilities HTTP/1.1\r\n
395 404 s> Accept-Encoding: identity\r\n
396 405 s> user-agent: test\r\n
397 406 s> host: $LOCALIP:$HGPORT\r\n (glob)
398 407 s> \r\n
399 408 s> makefile('rb', None)
400 409 s> HTTP/1.1 301 Redirect\r\n
401 410 s> Server: testing stub value\r\n
402 411 s> Date: $HTTP_DATE$\r\n
403 412 s> Location: http://$LOCALIP:$HGPORT/redirected?cmd=capabilities\r\n (glob)
404 413 s> Content-Type: text/plain\r\n
405 414 s> Content-Length: 10\r\n
406 415 s> \r\n
407 416 s> redirected
408 417 s> GET /redirected?cmd=capabilities HTTP/1.1\r\n
409 418 s> Accept-Encoding: identity\r\n
410 419 s> user-agent: test\r\n
411 420 s> host: $LOCALIP:$HGPORT\r\n (glob)
412 421 s> \r\n
413 422 s> makefile('rb', None)
414 423 s> HTTP/1.1 200 Script output follows\r\n
415 424 s> Server: testing stub value\r\n
416 425 s> Date: $HTTP_DATE$\r\n
417 426 s> Content-Type: application/mercurial-0.1\r\n
418 427 s> Content-Length: 453\r\n
419 428 s> \r\n
420 429 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
421 430
422 431 Test with the HTTP peer
423 432
424 433 $ hg --verbose debugwireproto http://$LOCALIP:$HGPORT/redirector << EOF
425 434 > command heads
426 435 > EOF
427 436 s> GET /redirector?cmd=capabilities HTTP/1.1\r\n
428 437 s> Accept-Encoding: identity\r\n
429 438 s> accept: application/mercurial-0.1\r\n
430 439 s> host: $LOCALIP:$HGPORT\r\n (glob)
431 440 s> user-agent: Mercurial debugwireproto\r\n
432 441 s> \r\n
433 442 s> makefile('rb', None)
434 443 s> HTTP/1.1 301 Redirect\r\n
435 444 s> Server: testing stub value\r\n
436 445 s> Date: $HTTP_DATE$\r\n
437 446 s> Location: http://$LOCALIP:$HGPORT/redirected?cmd=capabilities\r\n (glob)
438 447 s> Content-Type: text/plain\r\n
439 448 s> Content-Length: 10\r\n
440 449 s> \r\n
441 450 s> redirected
442 451 s> GET /redirected?cmd=capabilities HTTP/1.1\r\n
443 452 s> Accept-Encoding: identity\r\n
444 453 s> accept: application/mercurial-0.1\r\n
445 454 s> host: $LOCALIP:$HGPORT\r\n (glob)
446 455 s> user-agent: Mercurial debugwireproto\r\n
447 456 s> \r\n
448 457 s> makefile('rb', None)
449 458 s> HTTP/1.1 200 Script output follows\r\n
450 459 s> Server: testing stub value\r\n
451 460 s> Date: $HTTP_DATE$\r\n
452 461 s> Content-Type: application/mercurial-0.1\r\n
453 462 s> Content-Length: 453\r\n
454 463 s> \r\n
455 464 real URL is http://$LOCALIP:$HGPORT/redirected (glob)
456 465 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
457 466 sending heads command
458 467 s> GET /redirected?cmd=heads HTTP/1.1\r\n
459 468 s> Accept-Encoding: identity\r\n
460 469 s> vary: X-HgProto-1\r\n
461 470 s> x-hgproto-1: 0.1 0.2 comp=$USUAL_COMPRESSIONS$ partial-pull\r\n
462 471 s> accept: application/mercurial-0.1\r\n
463 472 s> host: $LOCALIP:$HGPORT\r\n (glob)
464 473 s> user-agent: Mercurial debugwireproto\r\n
465 474 s> \r\n
466 475 s> makefile('rb', None)
467 476 s> HTTP/1.1 200 Script output follows\r\n
468 477 s> Server: testing stub value\r\n
469 478 s> Date: $HTTP_DATE$\r\n
470 479 s> Content-Type: application/mercurial-0.1\r\n
471 480 s> Content-Length: 41\r\n
472 481 s> \r\n
473 482 s> 96ee1d7354c4ad7372047672c36a1f561e3a6a4c\n
474 483 response: [
475 484 b'\x96\xee\x1dsT\xc4\xadsr\x04vr\xc3j\x1fV\x1e:jL'
476 485 ]
477 486
478 487 $ killdaemons.py
479 488
480 489 Now test a variation where we strip the query string from the redirect URL.
481 490 (SCM Manager apparently did this and clients would recover from it)
482 491
483 492 $ hg --config extensions.redirect=$TESTTMP/redirectext.py \
484 493 > --config server.compressionengines=zlib \
485 494 > --config testing.redirectqs=false \
486 495 > serve --web-conf paths.conf --pid-file hg.pid -p $HGPORT -d
487 496 $ cat hg.pid > $DAEMON_PIDS
488 497
489 498 $ hg --verbose debugwireproto --peer raw http://$LOCALIP:$HGPORT << EOF
490 499 > httprequest GET /redirector?cmd=capabilities
491 500 > user-agent: test
492 501 > EOF
493 502 using raw connection to peer
494 503 s> GET /redirector?cmd=capabilities HTTP/1.1\r\n
495 504 s> Accept-Encoding: identity\r\n
496 505 s> user-agent: test\r\n
497 506 s> host: $LOCALIP:$HGPORT\r\n (glob)
498 507 s> \r\n
499 508 s> makefile('rb', None)
500 509 s> HTTP/1.1 301 Redirect\r\n
501 510 s> Server: testing stub value\r\n
502 511 s> Date: $HTTP_DATE$\r\n
503 512 s> Location: http://$LOCALIP:$HGPORT/redirected\r\n (glob)
504 513 s> Content-Type: text/plain\r\n
505 514 s> Content-Length: 10\r\n
506 515 s> \r\n
507 516 s> redirected
508 517 s> GET /redirected HTTP/1.1\r\n
509 518 s> Accept-Encoding: identity\r\n
510 519 s> user-agent: test\r\n
511 520 s> host: $LOCALIP:$HGPORT\r\n (glob)
512 521 s> \r\n
513 522 s> makefile('rb', None)
514 523 s> HTTP/1.1 200 Script output follows\r\n
515 524 s> Server: testing stub value\r\n
516 525 s> Date: $HTTP_DATE$\r\n
517 526 s> ETag: W/"*"\r\n (glob)
518 527 s> Content-Type: text/html; charset=ascii\r\n
519 528 s> Transfer-Encoding: chunked\r\n
520 529 s> \r\n
521 530 s> 414\r\n
522 531 s> <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN" "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">\n
523 532 s> <html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en-US">\n
524 533 s> <head>\n
525 534 s> <link rel="icon" href="/redirected/static/hgicon.png" type="image/png" />\n
526 535 s> <meta name="robots" content="index, nofollow" />\n
527 536 s> <link rel="stylesheet" href="/redirected/static/style-paper.css" type="text/css" />\n
528 537 s> <script type="text/javascript" src="/redirected/static/mercurial.js"></script>\n
529 538 s> \n
530 539 s> <title>redirected: log</title>\n
531 540 s> <link rel="alternate" type="application/atom+xml"\n
532 541 s> href="/redirected/atom-log" title="Atom feed for redirected" />\n
533 542 s> <link rel="alternate" type="application/rss+xml"\n
534 543 s> href="/redirected/rss-log" title="RSS feed for redirected" />\n
535 544 s> </head>\n
536 545 s> <body>\n
537 546 s> \n
538 547 s> <div class="container">\n
539 548 s> <div class="menu">\n
540 549 s> <div class="logo">\n
541 550 s> <a href="https://mercurial-scm.org/">\n
542 551 s> <img src="/redirected/static/hglogo.png" alt="mercurial" /></a>\n
543 552 s> </div>\n
544 553 s> <ul>\n
545 554 s> <li class="active">log</li>\n
546 555 s> <li><a href="/redirected/graph/tip">graph</a></li>\n
547 556 s> <li><a href="/redirected/tags">tags</a></li>\n
548 557 s> <li><a href="
549 558 s> \r\n
550 559 s> 810\r\n
551 560 s> /redirected/bookmarks">bookmarks</a></li>\n
552 561 s> <li><a href="/redirected/branches">branches</a></li>\n
553 562 s> </ul>\n
554 563 s> <ul>\n
555 564 s> <li><a href="/redirected/rev/tip">changeset</a></li>\n
556 565 s> <li><a href="/redirected/file/tip">browse</a></li>\n
557 566 s> </ul>\n
558 567 s> <ul>\n
559 568 s> \n
560 569 s> </ul>\n
561 570 s> <ul>\n
562 571 s> <li><a href="/redirected/help">help</a></li>\n
563 572 s> </ul>\n
564 573 s> <div class="atom-logo">\n
565 574 s> <a href="/redirected/atom-log" title="subscribe to atom feed">\n
566 575 s> <img class="atom-logo" src="/redirected/static/feed-icon-14x14.png" alt="atom feed" />\n
567 576 s> </a>\n
568 577 s> </div>\n
569 578 s> </div>\n
570 579 s> \n
571 580 s> <div class="main">\n
572 581 s> <h2 class="breadcrumb"><a href="/">Mercurial</a> &gt; <a href="/redirected">redirected</a> </h2>\n
573 582 s> <h3>log</h3>\n
574 583 s> \n
575 584 s> \n
576 585 s> <form class="search" action="/redirected/log">\n
577 586 s> \n
578 587 s> <p><input name="rev" id="search1" type="text" size="30" value="" /></p>\n
579 588 s> <div id="hint">Find changesets by keywords (author, files, the commit message), revision\n
580 589 s> number or hash, or <a href="/redirected/help/revsets">revset expression</a>.</div>\n
581 590 s> </form>\n
582 591 s> \n
583 592 s> <div class="navigate">\n
584 593 s> <a href="/redirected/shortlog/tip?revcount=30">less</a>\n
585 594 s> <a href="/redirected/shortlog/tip?revcount=120">more</a>\n
586 595 s> | rev 0: <a href="/redirected/shortlog/96ee1d7354c4">(0)</a> <a href="/redirected/shortlog/tip">tip</a> \n
587 596 s> </div>\n
588 597 s> \n
589 598 s> <table class="bigtable">\n
590 599 s> <thead>\n
591 600 s> <tr>\n
592 601 s> <th class="age">age</th>\n
593 602 s> <th class="author">author</th>\n
594 603 s> <th class="description">description</th>\n
595 604 s> </tr>\n
596 605 s> </thead>\n
597 606 s> <tbody class="stripes2">\n
598 607 s> <tr>\n
599 608 s> <td class="age">Thu, 01 Jan 1970 00:00:00 +0000</td>\n
600 609 s> <td class="author">test</td>\n
601 610 s> <td class="description">\n
602 611 s> <a href="/redirected/rev/96ee1d7354c4">initial</a>\n
603 612 s> <span class="phase">draft</span> <span class="branchhead">default</span> <span class="tag">tip</span> \n
604 613 s> </td>\n
605 614 s> </tr>\n
606 615 s> \n
607 616 s> </tbody>\n
608 617 s> </table>\n
609 618 s> \n
610 619 s> <div class="navigate">\n
611 620 s> <a href="/redirected/shortlog/tip?revcount=30">less</a>\n
612 621 s> <a href="/redirected/shortlog/tip?revcount=120">more</a>\n
613 622 s> | rev 0: <a href="/redirected/shortlog/96ee1d7354c4">(0)</a> <a href="/redirected/shortlog/tip">tip</a> \n
614 623 s> </div>\n
615 624 s> \n
616 625 s> <script type="text/javascript">\n
617 626 s> ajaxScrollInit(\n
618 627 s> \'/redirected/shortlog/%next%\',\n
619 628 s> \'\', <!-- NEXTHASH\n
620 629 s> function (htmlText) {
621 630 s> \r\n
622 631 s> 14a\r\n
623 632 s> \n
624 633 s> var m = htmlText.match(/\'(\\w+)\', <!-- NEXTHASH/);\n
625 634 s> return m ? m[1] : null;\n
626 635 s> },\n
627 636 s> \'.bigtable > tbody\',\n
628 637 s> \'<tr class="%class%">\\\n
629 638 s> <td colspan="3" style="text-align: center;">%text%</td>\\\n
630 639 s> </tr>\'\n
631 640 s> );\n
632 641 s> </script>\n
633 642 s> \n
634 643 s> </div>\n
635 644 s> </div>\n
636 645 s> \n
637 646 s> \n
638 647 s> \n
639 648 s> </body>\n
640 649 s> </html>\n
641 650 s> \n
642 651 s> \r\n
643 652 s> 0\r\n
644 653 s> \r\n
645 654
646 655 $ hg --verbose debugwireproto http://$LOCALIP:$HGPORT/redirector << EOF
647 656 > command heads
648 657 > EOF
649 658 s> GET /redirector?cmd=capabilities HTTP/1.1\r\n
650 659 s> Accept-Encoding: identity\r\n
651 660 s> accept: application/mercurial-0.1\r\n
652 661 s> host: $LOCALIP:$HGPORT\r\n (glob)
653 662 s> user-agent: Mercurial debugwireproto\r\n
654 663 s> \r\n
655 664 s> makefile('rb', None)
656 665 s> HTTP/1.1 301 Redirect\r\n
657 666 s> Server: testing stub value\r\n
658 667 s> Date: $HTTP_DATE$\r\n
659 668 s> Location: http://$LOCALIP:$HGPORT/redirected\r\n (glob)
660 669 s> Content-Type: text/plain\r\n
661 670 s> Content-Length: 10\r\n
662 671 s> \r\n
663 672 s> redirected
664 673 s> GET /redirected HTTP/1.1\r\n
665 674 s> Accept-Encoding: identity\r\n
666 675 s> accept: application/mercurial-0.1\r\n
667 676 s> host: $LOCALIP:$HGPORT\r\n (glob)
668 677 s> user-agent: Mercurial debugwireproto\r\n
669 678 s> \r\n
670 679 s> makefile('rb', None)
671 680 s> HTTP/1.1 200 Script output follows\r\n
672 681 s> Server: testing stub value\r\n
673 682 s> Date: $HTTP_DATE$\r\n
674 683 s> ETag: W/"*"\r\n (glob)
675 684 s> Content-Type: text/html; charset=ascii\r\n
676 685 s> Transfer-Encoding: chunked\r\n
677 686 s> \r\n
678 687 real URL is http://$LOCALIP:$HGPORT/redirected (glob)
679 688 s> 414\r\n
680 689 s> <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN" "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">\n
681 690 s> <html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en-US">\n
682 691 s> <head>\n
683 692 s> <link rel="icon" href="/redirected/static/hgicon.png" type="image/png" />\n
684 693 s> <meta name="robots" content="index, nofollow" />\n
685 694 s> <link rel="stylesheet" href="/redirected/static/style-paper.css" type="text/css" />\n
686 695 s> <script type="text/javascript" src="/redirected/static/mercurial.js"></script>\n
687 696 s> \n
688 697 s> <title>redirected: log</title>\n
689 698 s> <link rel="alternate" type="application/atom+xml"\n
690 699 s> href="/redirected/atom-log" title="Atom feed for redirected" />\n
691 700 s> <link rel="alternate" type="application/rss+xml"\n
692 701 s> href="/redirected/rss-log" title="RSS feed for redirected" />\n
693 702 s> </head>\n
694 703 s> <body>\n
695 704 s> \n
696 705 s> <div class="container">\n
697 706 s> <div class="menu">\n
698 707 s> <div class="logo">\n
699 708 s> <a href="https://mercurial-scm.org/">\n
700 709 s> <img src="/redirected/static/hglogo.png" alt="mercurial" /></a>\n
701 710 s> </div>\n
702 711 s> <ul>\n
703 712 s> <li class="active">log</li>\n
704 713 s> <li><a href="/redirected/graph/tip">graph</a></li>\n
705 714 s> <li><a href="/redirected/tags">tags</a
706 715 s> GET /redirected?cmd=capabilities HTTP/1.1\r\n
707 716 s> Accept-Encoding: identity\r\n
708 717 s> accept: application/mercurial-0.1\r\n
709 718 s> host: $LOCALIP:$HGPORT\r\n (glob)
710 719 s> user-agent: Mercurial debugwireproto\r\n
711 720 s> \r\n
712 721 s> makefile('rb', None)
713 722 s> HTTP/1.1 200 Script output follows\r\n
714 723 s> Server: testing stub value\r\n
715 724 s> Date: $HTTP_DATE$\r\n
716 725 s> Content-Type: application/mercurial-0.1\r\n
717 726 s> Content-Length: 453\r\n
718 727 s> \r\n
719 728 real URL is http://$LOCALIP:$HGPORT/redirected (glob)
720 729 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
721 730 sending heads command
722 731 s> GET /redirected?cmd=heads HTTP/1.1\r\n
723 732 s> Accept-Encoding: identity\r\n
724 733 s> vary: X-HgProto-1\r\n
725 734 s> x-hgproto-1: 0.1 0.2 comp=$USUAL_COMPRESSIONS$ partial-pull\r\n
726 735 s> accept: application/mercurial-0.1\r\n
727 736 s> host: $LOCALIP:$HGPORT\r\n (glob)
728 737 s> user-agent: Mercurial debugwireproto\r\n
729 738 s> \r\n
730 739 s> makefile('rb', None)
731 740 s> HTTP/1.1 200 Script output follows\r\n
732 741 s> Server: testing stub value\r\n
733 742 s> Date: $HTTP_DATE$\r\n
734 743 s> Content-Type: application/mercurial-0.1\r\n
735 744 s> Content-Length: 41\r\n
736 745 s> \r\n
737 746 s> 96ee1d7354c4ad7372047672c36a1f561e3a6a4c\n
738 747 response: [
739 748 b'\x96\xee\x1dsT\xc4\xadsr\x04vr\xc3j\x1fV\x1e:jL'
740 749 ]
@@ -1,83 +1,92
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 48 s> accept: application/mercurial-exp-framing-0005\r\n
49 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 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> 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
62 s> 13\r\n
63 s> \x0b\x00\x00\x01\x00\x02\x011
64 s> \xa1FstatusBok
65 s> \r\n
66 received frame(size=11; request=1; stream=2; streamflags=stream-begin; type=command-response; flags=continuation)
67 s> 78\r\n
68 s> p\x00\x00\x01\x00\x02\x001
69 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
65 70 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 71 s> \r\n
67 received frame(size=123; request=1; stream=2; streamflags=stream-begin; type=command-response; flags=eos)
72 received frame(size=112; request=1; stream=2; streamflags=; type=command-response; flags=continuation)
73 s> 8\r\n
74 s> \x00\x00\x00\x01\x00\x02\x002
75 s> \r\n
68 76 s> 0\r\n
69 77 s> \r\n
78 received frame(size=0; request=1; stream=2; streamflags=; type=command-response; flags=eos)
70 79 response: {
71 80 b'branch1': [
72 81 b'\xb5\xfa\xac\xdf\xd2c7h\xcb1R3l\xc0\x953\x81&f\x88'
73 82 ],
74 83 b'branch2': [
75 84 b'"Aa\xc7X\x9a\xa4\x8f\xa8:H\xfe\xff^\x95\xb5j\xe3\'\xfc'
76 85 ],
77 86 b'default': [
78 87 b'&\x80Z\xba\x1e`\n\x82\xe96a\x14\x9f#\x13\x86j"\x1a{',
79 88 b'\xbe\x0e\xf7<\x17\xad\xe3\xfc\x89\xdcAp\x1e\xb9\xfc:\x91\xb5\x82\x82'
80 89 ]
81 90 }
82 91
83 92 $ cat error.log
@@ -1,422 +1,431
1 1 #require no-chg
2 2
3 3 $ . $TESTDIR/wireprotohelpers.sh
4 4
5 5 $ hg init server
6 6
7 7 zstd isn't present in plain builds. Make tests easier by removing
8 8 zstd from the equation.
9 9
10 10 $ cat >> server/.hg/hgrc << EOF
11 11 > [server]
12 12 > compressionengines = zlib
13 13 > EOF
14 14
15 15 $ hg -R server serve -p $HGPORT -d --pid-file hg.pid -E error.log
16 16 $ cat hg.pid > $DAEMON_PIDS
17 17
18 18 A normal capabilities request is serviced for version 1
19 19
20 20 $ sendhttpraw << EOF
21 21 > httprequest GET ?cmd=capabilities
22 22 > user-agent: test
23 23 > EOF
24 24 using raw connection to peer
25 25 s> GET /?cmd=capabilities HTTP/1.1\r\n
26 26 s> Accept-Encoding: identity\r\n
27 27 s> user-agent: test\r\n
28 28 s> host: $LOCALIP:$HGPORT\r\n (glob)
29 29 s> \r\n
30 30 s> makefile('rb', None)
31 31 s> HTTP/1.1 200 Script output follows\r\n
32 32 s> Server: testing stub value\r\n
33 33 s> Date: $HTTP_DATE$\r\n
34 34 s> Content-Type: application/mercurial-0.1\r\n
35 35 s> Content-Length: *\r\n (glob)
36 36 s> \r\n
37 37 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
38 38
39 39 A proper request without the API server enabled returns the legacy response
40 40
41 41 $ sendhttpraw << EOF
42 42 > httprequest GET ?cmd=capabilities
43 43 > user-agent: test
44 44 > x-hgupgrade-1: foo
45 45 > x-hgproto-1: cbor
46 46 > EOF
47 47 using raw connection to peer
48 48 s> GET /?cmd=capabilities HTTP/1.1\r\n
49 49 s> Accept-Encoding: identity\r\n
50 50 s> user-agent: test\r\n
51 51 s> x-hgproto-1: cbor\r\n
52 52 s> x-hgupgrade-1: foo\r\n
53 53 s> host: $LOCALIP:$HGPORT\r\n (glob)
54 54 s> \r\n
55 55 s> makefile('rb', None)
56 56 s> HTTP/1.1 200 Script output follows\r\n
57 57 s> Server: testing stub value\r\n
58 58 s> Date: $HTTP_DATE$\r\n
59 59 s> Content-Type: application/mercurial-0.1\r\n
60 60 s> Content-Length: *\r\n (glob)
61 61 s> \r\n
62 62 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
63 63
64 64 Restart with just API server enabled. This enables serving the new format.
65 65
66 66 $ killdaemons.py
67 67 $ cat error.log
68 68
69 69 $ cat >> server/.hg/hgrc << EOF
70 70 > [experimental]
71 71 > web.apiserver = true
72 72 > EOF
73 73
74 74 $ hg -R server serve -p $HGPORT -d --pid-file hg.pid -E error.log
75 75 $ cat hg.pid > $DAEMON_PIDS
76 76
77 77 X-HgUpgrade-<N> without CBOR advertisement uses legacy response
78 78
79 79 $ sendhttpraw << EOF
80 80 > httprequest GET ?cmd=capabilities
81 81 > user-agent: test
82 82 > x-hgupgrade-1: foo bar
83 83 > EOF
84 84 using raw connection to peer
85 85 s> GET /?cmd=capabilities HTTP/1.1\r\n
86 86 s> Accept-Encoding: identity\r\n
87 87 s> user-agent: test\r\n
88 88 s> x-hgupgrade-1: foo bar\r\n
89 89 s> host: $LOCALIP:$HGPORT\r\n (glob)
90 90 s> \r\n
91 91 s> makefile('rb', None)
92 92 s> HTTP/1.1 200 Script output follows\r\n
93 93 s> Server: testing stub value\r\n
94 94 s> Date: $HTTP_DATE$\r\n
95 95 s> Content-Type: application/mercurial-0.1\r\n
96 96 s> Content-Length: *\r\n (glob)
97 97 s> \r\n
98 98 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
99 99
100 100 X-HgUpgrade-<N> without known serialization in X-HgProto-<N> uses legacy response
101 101
102 102 $ sendhttpraw << EOF
103 103 > httprequest GET ?cmd=capabilities
104 104 > user-agent: test
105 105 > x-hgupgrade-1: foo bar
106 106 > x-hgproto-1: some value
107 107 > EOF
108 108 using raw connection to peer
109 109 s> GET /?cmd=capabilities HTTP/1.1\r\n
110 110 s> Accept-Encoding: identity\r\n
111 111 s> user-agent: test\r\n
112 112 s> x-hgproto-1: some value\r\n
113 113 s> x-hgupgrade-1: foo bar\r\n
114 114 s> host: $LOCALIP:$HGPORT\r\n (glob)
115 115 s> \r\n
116 116 s> makefile('rb', None)
117 117 s> HTTP/1.1 200 Script output follows\r\n
118 118 s> Server: testing stub value\r\n
119 119 s> Date: $HTTP_DATE$\r\n
120 120 s> Content-Type: application/mercurial-0.1\r\n
121 121 s> Content-Length: *\r\n (glob)
122 122 s> \r\n
123 123 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
124 124
125 125 X-HgUpgrade-<N> + X-HgProto-<N> headers trigger new response format
126 126
127 127 $ sendhttpraw << EOF
128 128 > httprequest GET ?cmd=capabilities
129 129 > user-agent: test
130 130 > x-hgupgrade-1: foo bar
131 131 > x-hgproto-1: cbor
132 132 > EOF
133 133 using raw connection to peer
134 134 s> GET /?cmd=capabilities HTTP/1.1\r\n
135 135 s> Accept-Encoding: identity\r\n
136 136 s> user-agent: test\r\n
137 137 s> x-hgproto-1: cbor\r\n
138 138 s> x-hgupgrade-1: foo bar\r\n
139 139 s> host: $LOCALIP:$HGPORT\r\n (glob)
140 140 s> \r\n
141 141 s> makefile('rb', None)
142 142 s> HTTP/1.1 200 OK\r\n
143 143 s> Server: testing stub value\r\n
144 144 s> Date: $HTTP_DATE$\r\n
145 145 s> Content-Type: application/mercurial-cbor\r\n
146 146 s> Content-Length: *\r\n (glob)
147 147 s> \r\n
148 148 s> \xa3GapibaseDapi/Dapis\xa0Nv1capabilitiesY\x01\xc5batch 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
149 149 cbor> {
150 150 b'apibase': b'api/',
151 151 b'apis': {},
152 152 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'
153 153 }
154 154
155 155 Restart server to enable HTTPv2
156 156
157 157 $ killdaemons.py
158 158 $ enablehttpv2 server
159 159 $ hg -R server serve -p $HGPORT -d --pid-file hg.pid -E error.log
160 160 $ cat hg.pid > $DAEMON_PIDS
161 161
162 162 Only requested API services are returned
163 163
164 164 $ sendhttpraw << EOF
165 165 > httprequest GET ?cmd=capabilities
166 166 > user-agent: test
167 167 > x-hgupgrade-1: foo bar
168 168 > x-hgproto-1: cbor
169 169 > EOF
170 170 using raw connection to peer
171 171 s> GET /?cmd=capabilities HTTP/1.1\r\n
172 172 s> Accept-Encoding: identity\r\n
173 173 s> user-agent: test\r\n
174 174 s> x-hgproto-1: cbor\r\n
175 175 s> x-hgupgrade-1: foo bar\r\n
176 176 s> host: $LOCALIP:$HGPORT\r\n (glob)
177 177 s> \r\n
178 178 s> makefile('rb', None)
179 179 s> HTTP/1.1 200 OK\r\n
180 180 s> Server: testing stub value\r\n
181 181 s> Date: $HTTP_DATE$\r\n
182 182 s> Content-Type: application/mercurial-cbor\r\n
183 183 s> Content-Length: *\r\n (glob)
184 184 s> \r\n
185 185 s> \xa3GapibaseDapi/Dapis\xa0Nv1capabilitiesY\x01\xc5batch 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
186 186 cbor> {
187 187 b'apibase': b'api/',
188 188 b'apis': {},
189 189 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'
190 190 }
191 191
192 192 Request for HTTPv2 service returns information about it
193 193
194 194 $ sendhttpraw << EOF
195 195 > httprequest GET ?cmd=capabilities
196 196 > user-agent: test
197 197 > x-hgupgrade-1: exp-http-v2-0001 foo bar
198 198 > x-hgproto-1: cbor
199 199 > EOF
200 200 using raw connection to peer
201 201 s> GET /?cmd=capabilities HTTP/1.1\r\n
202 202 s> Accept-Encoding: identity\r\n
203 203 s> user-agent: test\r\n
204 204 s> x-hgproto-1: cbor\r\n
205 205 s> x-hgupgrade-1: exp-http-v2-0001 foo bar\r\n
206 206 s> host: $LOCALIP:$HGPORT\r\n (glob)
207 207 s> \r\n
208 208 s> makefile('rb', None)
209 209 s> HTTP/1.1 200 OK\r\n
210 210 s> Server: testing stub value\r\n
211 211 s> Date: $HTTP_DATE$\r\n
212 212 s> Content-Type: application/mercurial-cbor\r\n
213 213 s> Content-Length: *\r\n (glob)
214 214 s> \r\n
215 215 s> \xa3GapibaseDapi/Dapis\xa1Pexp-http-v2-0001\xa4Hcommands\xa7Ibranchmap\xa2Dargs\xa0Kpermissions\x81DpullLcapabilities\xa2Dargs\xa0Kpermissions\x81DpullEheads\xa2Dargs\xa1Jpubliconly\xf4Kpermissions\x81DpullEknown\xa2Dargs\xa1Enodes\x81HdeadbeefKpermissions\x81DpullHlistkeys\xa2Dargs\xa1InamespaceBnsKpermissions\x81DpullFlookup\xa2Dargs\xa1CkeyCfooKpermissions\x81DpullGpushkey\xa2Dargs\xa4CkeyCkeyInamespaceBnsCnewCnewColdColdKpermissions\x81DpushKcompression\x81\xa1DnameDzlibQframingmediatypes\x81X&application/mercurial-exp-framing-0005Nrawrepoformats\x82LgeneraldeltaHrevlogv1Nv1capabilitiesY\x01\xc5batch 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
216 216 cbor> {
217 217 b'apibase': b'api/',
218 218 b'apis': {
219 219 b'exp-http-v2-0001': {
220 220 b'commands': {
221 221 b'branchmap': {
222 222 b'args': {},
223 223 b'permissions': [
224 224 b'pull'
225 225 ]
226 226 },
227 227 b'capabilities': {
228 228 b'args': {},
229 229 b'permissions': [
230 230 b'pull'
231 231 ]
232 232 },
233 233 b'heads': {
234 234 b'args': {
235 235 b'publiconly': False
236 236 },
237 237 b'permissions': [
238 238 b'pull'
239 239 ]
240 240 },
241 241 b'known': {
242 242 b'args': {
243 243 b'nodes': [
244 244 b'deadbeef'
245 245 ]
246 246 },
247 247 b'permissions': [
248 248 b'pull'
249 249 ]
250 250 },
251 251 b'listkeys': {
252 252 b'args': {
253 253 b'namespace': b'ns'
254 254 },
255 255 b'permissions': [
256 256 b'pull'
257 257 ]
258 258 },
259 259 b'lookup': {
260 260 b'args': {
261 261 b'key': b'foo'
262 262 },
263 263 b'permissions': [
264 264 b'pull'
265 265 ]
266 266 },
267 267 b'pushkey': {
268 268 b'args': {
269 269 b'key': b'key',
270 270 b'namespace': b'ns',
271 271 b'new': b'new',
272 272 b'old': b'old'
273 273 },
274 274 b'permissions': [
275 275 b'push'
276 276 ]
277 277 }
278 278 },
279 279 b'compression': [
280 280 {
281 281 b'name': b'zlib'
282 282 }
283 283 ],
284 284 b'framingmediatypes': [
285 285 b'application/mercurial-exp-framing-0005'
286 286 ],
287 287 b'rawrepoformats': [
288 288 b'generaldelta',
289 289 b'revlogv1'
290 290 ]
291 291 }
292 292 },
293 293 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'
294 294 }
295 295
296 296 capabilities command returns expected info
297 297
298 298 $ sendhttpv2peerhandshake << EOF
299 299 > command capabilities
300 300 > EOF
301 301 creating http peer for wire protocol version 2
302 302 s> GET /?cmd=capabilities HTTP/1.1\r\n
303 303 s> Accept-Encoding: identity\r\n
304 304 s> vary: X-HgProto-1,X-HgUpgrade-1\r\n
305 305 s> x-hgproto-1: cbor\r\n
306 306 s> x-hgupgrade-1: exp-http-v2-0001\r\n
307 307 s> accept: application/mercurial-0.1\r\n
308 308 s> host: $LOCALIP:$HGPORT\r\n (glob)
309 309 s> user-agent: Mercurial debugwireproto\r\n
310 310 s> \r\n
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 315 s> Content-Type: application/mercurial-cbor\r\n
316 316 s> Content-Length: *\r\n (glob)
317 317 s> \r\n
318 318 s> \xa3GapibaseDapi/Dapis\xa1Pexp-http-v2-0001\xa4Hcommands\xa7Ibranchmap\xa2Dargs\xa0Kpermissions\x81DpullLcapabilities\xa2Dargs\xa0Kpermissions\x81DpullEheads\xa2Dargs\xa1Jpubliconly\xf4Kpermissions\x81DpullEknown\xa2Dargs\xa1Enodes\x81HdeadbeefKpermissions\x81DpullHlistkeys\xa2Dargs\xa1InamespaceBnsKpermissions\x81DpullFlookup\xa2Dargs\xa1CkeyCfooKpermissions\x81DpullGpushkey\xa2Dargs\xa4CkeyCkeyInamespaceBnsCnewCnewColdColdKpermissions\x81DpushKcompression\x81\xa1DnameDzlibQframingmediatypes\x81X&application/mercurial-exp-framing-0005Nrawrepoformats\x82LgeneraldeltaHrevlogv1Nv1capabilitiesY\x01\xc5batch 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
319 319 sending capabilities command
320 320 s> POST /api/exp-http-v2-0001/ro/capabilities HTTP/1.1\r\n
321 321 s> Accept-Encoding: identity\r\n
322 322 s> *\r\n (glob)
323 323 s> content-type: application/mercurial-exp-framing-0005\r\n
324 324 s> content-length: 27\r\n
325 325 s> host: $LOCALIP:$HGPORT\r\n (glob)
326 326 s> user-agent: Mercurial debugwireproto\r\n
327 327 s> \r\n
328 328 s> \x13\x00\x00\x01\x00\x01\x01\x11\xa1DnameLcapabilities
329 329 s> makefile('rb', None)
330 330 s> HTTP/1.1 200 OK\r\n
331 331 s> Server: testing stub value\r\n
332 332 s> Date: $HTTP_DATE$\r\n
333 333 s> Content-Type: application/mercurial-exp-framing-0005\r\n
334 334 s> Transfer-Encoding: chunked\r\n
335 335 s> \r\n
336 s> 1d7\r\n
337 s> \xcf\x01\x00\x01\x00\x02\x012
338 s> \xa1FstatusBok\xa4Hcommands\xa7Ibranchmap\xa2Dargs\xa0Kpermissions\x81DpullLcapabilities\xa2Dargs\xa0Kpermissions\x81DpullEheads\xa2Dargs\xa1Jpubliconly\xf4Kpermissions\x81DpullEknown\xa2Dargs\xa1Enodes\x81HdeadbeefKpermissions\x81DpullHlistkeys\xa2Dargs\xa1InamespaceBnsKpermissions\x81DpullFlookup\xa2Dargs\xa1CkeyCfooKpermissions\x81DpullGpushkey\xa2Dargs\xa4CkeyCkeyInamespaceBnsCnewCnewColdColdKpermissions\x81DpushKcompression\x81\xa1DnameDzlibQframingmediatypes\x81X&application/mercurial-exp-framing-0005Nrawrepoformats\x82LgeneraldeltaHrevlogv1
336 s> 13\r\n
337 s> \x0b\x00\x00\x01\x00\x02\x011
338 s> \xa1FstatusBok
339 339 s> \r\n
340 received frame(size=463; request=1; stream=2; streamflags=stream-begin; type=command-response; flags=eos)
340 received frame(size=11; request=1; stream=2; streamflags=stream-begin; type=command-response; flags=continuation)
341 s> 1cc\r\n
342 s> \xc4\x01\x00\x01\x00\x02\x001
343 s> \xa4Hcommands\xa7Ibranchmap\xa2Dargs\xa0Kpermissions\x81DpullLcapabilities\xa2Dargs\xa0Kpermissions\x81DpullEheads\xa2Dargs\xa1Jpubliconly\xf4Kpermissions\x81DpullEknown\xa2Dargs\xa1Enodes\x81HdeadbeefKpermissions\x81DpullHlistkeys\xa2Dargs\xa1InamespaceBnsKpermissions\x81DpullFlookup\xa2Dargs\xa1CkeyCfooKpermissions\x81DpullGpushkey\xa2Dargs\xa4CkeyCkeyInamespaceBnsCnewCnewColdColdKpermissions\x81DpushKcompression\x81\xa1DnameDzlibQframingmediatypes\x81X&application/mercurial-exp-framing-0005Nrawrepoformats\x82LgeneraldeltaHrevlogv1
344 s> \r\n
345 received frame(size=452; request=1; stream=2; streamflags=; type=command-response; flags=continuation)
346 s> 8\r\n
347 s> \x00\x00\x00\x01\x00\x02\x002
348 s> \r\n
341 349 s> 0\r\n
342 350 s> \r\n
351 received frame(size=0; request=1; stream=2; streamflags=; type=command-response; flags=eos)
343 352 response: [
344 353 {
345 354 b'status': b'ok'
346 355 },
347 356 {
348 357 b'commands': {
349 358 b'branchmap': {
350 359 b'args': {},
351 360 b'permissions': [
352 361 b'pull'
353 362 ]
354 363 },
355 364 b'capabilities': {
356 365 b'args': {},
357 366 b'permissions': [
358 367 b'pull'
359 368 ]
360 369 },
361 370 b'heads': {
362 371 b'args': {
363 372 b'publiconly': False
364 373 },
365 374 b'permissions': [
366 375 b'pull'
367 376 ]
368 377 },
369 378 b'known': {
370 379 b'args': {
371 380 b'nodes': [
372 381 b'deadbeef'
373 382 ]
374 383 },
375 384 b'permissions': [
376 385 b'pull'
377 386 ]
378 387 },
379 388 b'listkeys': {
380 389 b'args': {
381 390 b'namespace': b'ns'
382 391 },
383 392 b'permissions': [
384 393 b'pull'
385 394 ]
386 395 },
387 396 b'lookup': {
388 397 b'args': {
389 398 b'key': b'foo'
390 399 },
391 400 b'permissions': [
392 401 b'pull'
393 402 ]
394 403 },
395 404 b'pushkey': {
396 405 b'args': {
397 406 b'key': b'key',
398 407 b'namespace': b'ns',
399 408 b'new': b'new',
400 409 b'old': b'old'
401 410 },
402 411 b'permissions': [
403 412 b'push'
404 413 ]
405 414 }
406 415 },
407 416 b'compression': [
408 417 {
409 418 b'name': b'zlib'
410 419 }
411 420 ],
412 421 b'framingmediatypes': [
413 422 b'application/mercurial-exp-framing-0005'
414 423 ],
415 424 b'rawrepoformats': [
416 425 b'generaldelta',
417 426 b'revlogv1'
418 427 ]
419 428 }
420 429 ]
421 430
422 431 $ cat error.log
@@ -1,102 +1,120
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 40 s> accept: application/mercurial-exp-framing-0005\r\n
41 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 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> 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
54 s> 13\r\n
55 s> \x0b\x00\x00\x01\x00\x02\x011
56 s> \xa1FstatusBok
57 57 s> \r\n
58 received frame(size=75; request=1; stream=2; streamflags=stream-begin; type=command-response; flags=eos)
58 received frame(size=11; request=1; stream=2; streamflags=stream-begin; type=command-response; flags=continuation)
59 s> 48\r\n
60 s> @\x00\x00\x01\x00\x02\x001
61 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
62 s> \r\n
63 received frame(size=64; request=1; stream=2; streamflags=; type=command-response; flags=continuation)
64 s> 8\r\n
65 s> \x00\x00\x00\x01\x00\x02\x002
66 s> \r\n
59 67 s> 0\r\n
60 68 s> \r\n
69 received frame(size=0; request=1; stream=2; streamflags=; type=command-response; flags=eos)
61 70 response: [
62 71 b'\x1dok\x91\xd4J\xab\xa6\xd5\xe5\x80\xbc0\xa9\x94\x850\xdb\xe0\x0b',
63 72 b'\xaeI.6\xb0\xc83\x9f\xfa\xf3(\xd0\x0b\x85\xb4R]\xe1\x16^',
64 73 b')Dm-\xc5A\x9c_\x97Dz\x8b\xc0b\xe4\xcc2\x8b\xf2A'
65 74 ]
66 75
67 76 Requesting just the public heads works
68 77
69 78 $ sendhttpv2peer << EOF
70 79 > command heads
71 80 > publiconly 1
72 81 > EOF
73 82 creating http peer for wire protocol version 2
74 83 sending heads command
75 84 s> POST /api/exp-http-v2-0001/ro/heads HTTP/1.1\r\n
76 85 s> Accept-Encoding: identity\r\n
77 86 s> accept: application/mercurial-exp-framing-0005\r\n
78 87 s> content-type: application/mercurial-exp-framing-0005\r\n
79 88 s> content-length: 39\r\n
80 89 s> host: $LOCALIP:$HGPORT\r\n (glob)
81 90 s> user-agent: Mercurial debugwireproto\r\n
82 91 s> \r\n
83 92 s> \x1f\x00\x00\x01\x00\x01\x01\x11\xa2Dargs\xa1JpubliconlyA1DnameEheads
84 93 s> makefile('rb', None)
85 94 s> HTTP/1.1 200 OK\r\n
86 95 s> Server: testing stub value\r\n
87 96 s> Date: $HTTP_DATE$\r\n
88 97 s> Content-Type: application/mercurial-exp-framing-0005\r\n
89 98 s> Transfer-Encoding: chunked\r\n
90 99 s> \r\n
91 s> 29\r\n
92 s> !\x00\x00\x01\x00\x02\x012
93 s> \xa1FstatusBok\x81Tx\xd2\xdc\xa46\xb2\xf5\xb1\x88\xac&~)\xb8\x1e\x07&m8\xfc
100 s> 13\r\n
101 s> \x0b\x00\x00\x01\x00\x02\x011
102 s> \xa1FstatusBok
94 103 s> \r\n
95 received frame(size=33; request=1; stream=2; streamflags=stream-begin; type=command-response; flags=eos)
104 received frame(size=11; request=1; stream=2; streamflags=stream-begin; type=command-response; flags=continuation)
105 s> 1e\r\n
106 s> \x16\x00\x00\x01\x00\x02\x001
107 s> \x81Tx\xd2\xdc\xa46\xb2\xf5\xb1\x88\xac&~)\xb8\x1e\x07&m8\xfc
108 s> \r\n
109 received frame(size=22; request=1; stream=2; streamflags=; type=command-response; flags=continuation)
110 s> 8\r\n
111 s> \x00\x00\x00\x01\x00\x02\x002
112 s> \r\n
96 113 s> 0\r\n
97 114 s> \r\n
115 received frame(size=0; request=1; stream=2; streamflags=; type=command-response; flags=eos)
98 116 response: [
99 117 b'x\xd2\xdc\xa46\xb2\xf5\xb1\x88\xac&~)\xb8\x1e\x07&m8\xfc'
100 118 ]
101 119
102 120 $ cat error.log
@@ -1,127 +1,154
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 32 s> accept: application/mercurial-exp-framing-0005\r\n
33 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 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> 14\r\n
47 s> \x0c\x00\x00\x01\x00\x02\x012
48 s> \xa1FstatusBok@
46 s> 13\r\n
47 s> \x0b\x00\x00\x01\x00\x02\x011
48 s> \xa1FstatusBok
49 49 s> \r\n
50 received frame(size=12; request=1; stream=2; streamflags=stream-begin; type=command-response; flags=eos)
50 received frame(size=11; request=1; stream=2; streamflags=stream-begin; type=command-response; flags=continuation)
51 s> 9\r\n
52 s> \x01\x00\x00\x01\x00\x02\x001
53 s> @
54 s> \r\n
55 received frame(size=1; request=1; stream=2; streamflags=; type=command-response; flags=continuation)
56 s> 8\r\n
57 s> \x00\x00\x00\x01\x00\x02\x002
58 s> \r\n
51 59 s> 0\r\n
52 60 s> \r\n
61 received frame(size=0; request=1; stream=2; streamflags=; type=command-response; flags=eos)
53 62 response: []
54 63
55 64 Single known node works
56 65
57 66 $ sendhttpv2peer << EOF
58 67 > command known
59 68 > nodes eval:[b'\x42\x6b\xad\xa5\xc6\x75\x98\xca\x65\x03\x6d\x57\xd9\xe4\xb6\x4b\x0c\x1c\xe7\xa0']
60 69 > EOF
61 70 creating http peer for wire protocol version 2
62 71 sending known command
63 72 s> POST /api/exp-http-v2-0001/ro/known HTTP/1.1\r\n
64 73 s> Accept-Encoding: identity\r\n
65 74 s> accept: application/mercurial-exp-framing-0005\r\n
66 75 s> content-type: application/mercurial-exp-framing-0005\r\n
67 76 s> content-length: 54\r\n
68 77 s> host: $LOCALIP:$HGPORT\r\n (glob)
69 78 s> user-agent: Mercurial debugwireproto\r\n
70 79 s> \r\n
71 80 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 81 s> makefile('rb', None)
73 82 s> HTTP/1.1 200 OK\r\n
74 83 s> Server: testing stub value\r\n
75 84 s> Date: $HTTP_DATE$\r\n
76 85 s> Content-Type: application/mercurial-exp-framing-0005\r\n
77 86 s> Transfer-Encoding: chunked\r\n
78 87 s> \r\n
79 s> 15\r\n
80 s> \r\x00\x00\x01\x00\x02\x012
81 s> \xa1FstatusBokA1
88 s> 13\r\n
89 s> \x0b\x00\x00\x01\x00\x02\x011
90 s> \xa1FstatusBok
82 91 s> \r\n
83 received frame(size=13; request=1; stream=2; streamflags=stream-begin; type=command-response; flags=eos)
92 received frame(size=11; request=1; stream=2; streamflags=stream-begin; type=command-response; flags=continuation)
93 s> a\r\n
94 s> \x02\x00\x00\x01\x00\x02\x001
95 s> A1
96 s> \r\n
97 received frame(size=2; request=1; stream=2; streamflags=; type=command-response; flags=continuation)
98 s> 8\r\n
99 s> \x00\x00\x00\x01\x00\x02\x002
100 s> \r\n
84 101 s> 0\r\n
85 102 s> \r\n
103 received frame(size=0; request=1; stream=2; streamflags=; type=command-response; flags=eos)
86 104 response: [
87 105 True
88 106 ]
89 107
90 108 Multiple nodes works
91 109
92 110 $ sendhttpv2peer << EOF
93 111 > command known
94 112 > 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']
95 113 > EOF
96 114 creating http peer for wire protocol version 2
97 115 sending known command
98 116 s> POST /api/exp-http-v2-0001/ro/known HTTP/1.1\r\n
99 117 s> Accept-Encoding: identity\r\n
100 118 s> accept: application/mercurial-exp-framing-0005\r\n
101 119 s> content-type: application/mercurial-exp-framing-0005\r\n
102 120 s> content-length: 96\r\n
103 121 s> host: $LOCALIP:$HGPORT\r\n (glob)
104 122 s> user-agent: Mercurial debugwireproto\r\n
105 123 s> \r\n
106 124 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
107 125 s> makefile('rb', None)
108 126 s> HTTP/1.1 200 OK\r\n
109 127 s> Server: testing stub value\r\n
110 128 s> Date: $HTTP_DATE$\r\n
111 129 s> Content-Type: application/mercurial-exp-framing-0005\r\n
112 130 s> Transfer-Encoding: chunked\r\n
113 131 s> \r\n
114 s> 17\r\n
115 s> \x0f\x00\x00\x01\x00\x02\x012
116 s> \xa1FstatusBokC101
132 s> 13\r\n
133 s> \x0b\x00\x00\x01\x00\x02\x011
134 s> \xa1FstatusBok
117 135 s> \r\n
118 received frame(size=15; request=1; stream=2; streamflags=stream-begin; type=command-response; flags=eos)
136 received frame(size=11; request=1; stream=2; streamflags=stream-begin; type=command-response; flags=continuation)
137 s> c\r\n
138 s> \x04\x00\x00\x01\x00\x02\x001
139 s> C101
140 s> \r\n
141 received frame(size=4; request=1; stream=2; streamflags=; type=command-response; flags=continuation)
142 s> 8\r\n
143 s> \x00\x00\x00\x01\x00\x02\x002
144 s> \r\n
119 145 s> 0\r\n
120 146 s> \r\n
147 received frame(size=0; request=1; stream=2; streamflags=; type=command-response; flags=eos)
121 148 response: [
122 149 True,
123 150 False,
124 151 True
125 152 ]
126 153
127 154 $ cat error.log
@@ -1,134 +1,161
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 36 s> accept: application/mercurial-exp-framing-0005\r\n
37 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 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> 33\r\n
51 s> +\x00\x00\x01\x00\x02\x012
52 s> \xa1FstatusBok\xa3Ibookmarks@Jnamespaces@Fphases@
50 s> 13\r\n
51 s> \x0b\x00\x00\x01\x00\x02\x011
52 s> \xa1FstatusBok
53 53 s> \r\n
54 received frame(size=43; request=1; stream=2; streamflags=stream-begin; type=command-response; flags=eos)
54 received frame(size=11; request=1; stream=2; streamflags=stream-begin; type=command-response; flags=continuation)
55 s> 28\r\n
56 s> \x00\x00\x01\x00\x02\x001
57 s> \xa3Ibookmarks@Jnamespaces@Fphases@
58 s> \r\n
59 received frame(size=32; request=1; stream=2; streamflags=; type=command-response; flags=continuation)
60 s> 8\r\n
61 s> \x00\x00\x00\x01\x00\x02\x002
62 s> \r\n
55 63 s> 0\r\n
56 64 s> \r\n
65 received frame(size=0; request=1; stream=2; streamflags=; type=command-response; flags=eos)
57 66 response: {
58 67 b'bookmarks': b'',
59 68 b'namespaces': b'',
60 69 b'phases': b''
61 70 }
62 71
63 72 Request for phases works
64 73
65 74 $ sendhttpv2peer << EOF
66 75 > command listkeys
67 76 > namespace phases
68 77 > EOF
69 78 creating http peer for wire protocol version 2
70 79 sending listkeys command
71 80 s> POST /api/exp-http-v2-0001/ro/listkeys HTTP/1.1\r\n
72 81 s> Accept-Encoding: identity\r\n
73 82 s> accept: application/mercurial-exp-framing-0005\r\n
74 83 s> content-type: application/mercurial-exp-framing-0005\r\n
75 84 s> content-length: 46\r\n
76 85 s> host: $LOCALIP:$HGPORT\r\n (glob)
77 86 s> user-agent: Mercurial debugwireproto\r\n
78 87 s> \r\n
79 88 s> &\x00\x00\x01\x00\x01\x01\x11\xa2Dargs\xa1InamespaceFphasesDnameHlistkeys
80 89 s> makefile('rb', None)
81 90 s> HTTP/1.1 200 OK\r\n
82 91 s> Server: testing stub value\r\n
83 92 s> Date: $HTTP_DATE$\r\n
84 93 s> Content-Type: application/mercurial-exp-framing-0005\r\n
85 94 s> Transfer-Encoding: chunked\r\n
86 95 s> \r\n
87 s> 50\r\n
88 s> H\x00\x00\x01\x00\x02\x012
89 s> \xa1FstatusBok\xa2X(be0ef73c17ade3fc89dc41701eb9fc3a91b58282A1JpublishingDTrue
96 s> 13\r\n
97 s> \x0b\x00\x00\x01\x00\x02\x011
98 s> \xa1FstatusBok
90 99 s> \r\n
91 received frame(size=72; request=1; stream=2; streamflags=stream-begin; type=command-response; flags=eos)
100 received frame(size=11; request=1; stream=2; streamflags=stream-begin; type=command-response; flags=continuation)
101 s> 45\r\n
102 s> =\x00\x00\x01\x00\x02\x001
103 s> \xa2X(be0ef73c17ade3fc89dc41701eb9fc3a91b58282A1JpublishingDTrue
104 s> \r\n
105 received frame(size=61; request=1; stream=2; streamflags=; type=command-response; flags=continuation)
106 s> 8\r\n
107 s> \x00\x00\x00\x01\x00\x02\x002
108 s> \r\n
92 109 s> 0\r\n
93 110 s> \r\n
111 received frame(size=0; request=1; stream=2; streamflags=; type=command-response; flags=eos)
94 112 response: {
95 113 b'be0ef73c17ade3fc89dc41701eb9fc3a91b58282': b'1',
96 114 b'publishing': b'True'
97 115 }
98 116
99 117 Request for bookmarks works
100 118
101 119 $ sendhttpv2peer << EOF
102 120 > command listkeys
103 121 > namespace bookmarks
104 122 > EOF
105 123 creating http peer for wire protocol version 2
106 124 sending listkeys command
107 125 s> POST /api/exp-http-v2-0001/ro/listkeys HTTP/1.1\r\n
108 126 s> Accept-Encoding: identity\r\n
109 127 s> accept: application/mercurial-exp-framing-0005\r\n
110 128 s> content-type: application/mercurial-exp-framing-0005\r\n
111 129 s> content-length: 49\r\n
112 130 s> host: $LOCALIP:$HGPORT\r\n (glob)
113 131 s> user-agent: Mercurial debugwireproto\r\n
114 132 s> \r\n
115 133 s> )\x00\x00\x01\x00\x01\x01\x11\xa2Dargs\xa1InamespaceIbookmarksDnameHlistkeys
116 134 s> makefile('rb', None)
117 135 s> HTTP/1.1 200 OK\r\n
118 136 s> Server: testing stub value\r\n
119 137 s> Date: $HTTP_DATE$\r\n
120 138 s> Content-Type: application/mercurial-exp-framing-0005\r\n
121 139 s> Transfer-Encoding: chunked\r\n
122 140 s> \r\n
123 s> 40\r\n
124 s> 8\x00\x00\x01\x00\x02\x012
125 s> \xa1FstatusBok\xa1A@X(26805aba1e600a82e93661149f2313866a221a7b
141 s> 13\r\n
142 s> \x0b\x00\x00\x01\x00\x02\x011
143 s> \xa1FstatusBok
126 144 s> \r\n
127 received frame(size=56; request=1; stream=2; streamflags=stream-begin; type=command-response; flags=eos)
145 received frame(size=11; request=1; stream=2; streamflags=stream-begin; type=command-response; flags=continuation)
146 s> 35\r\n
147 s> -\x00\x00\x01\x00\x02\x001
148 s> \xa1A@X(26805aba1e600a82e93661149f2313866a221a7b
149 s> \r\n
150 received frame(size=45; request=1; stream=2; streamflags=; type=command-response; flags=continuation)
151 s> 8\r\n
152 s> \x00\x00\x00\x01\x00\x02\x002
153 s> \r\n
128 154 s> 0\r\n
129 155 s> \r\n
156 received frame(size=0; request=1; stream=2; streamflags=; type=command-response; flags=eos)
130 157 response: {
131 158 b'@': b'26805aba1e600a82e93661149f2313866a221a7b'
132 159 }
133 160
134 161 $ cat error.log
@@ -1,55 +1,64
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 32 s> accept: application/mercurial-exp-framing-0005\r\n
33 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 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> 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
46 s> 13\r\n
47 s> \x0b\x00\x00\x01\x00\x02\x011
48 s> \xa1FstatusBok
49 49 s> \r\n
50 received frame(size=32; request=1; stream=2; streamflags=stream-begin; type=command-response; flags=eos)
50 received frame(size=11; request=1; stream=2; streamflags=stream-begin; type=command-response; flags=continuation)
51 s> 1d\r\n
52 s> \x15\x00\x00\x01\x00\x02\x001
53 s> TBk\xad\xa5\xc6u\x98\xcae\x03mW\xd9\xe4\xb6K\x0c\x1c\xe7\xa0
54 s> \r\n
55 received frame(size=21; request=1; stream=2; streamflags=; type=command-response; flags=continuation)
56 s> 8\r\n
57 s> \x00\x00\x00\x01\x00\x02\x002
58 s> \r\n
51 59 s> 0\r\n
52 60 s> \r\n
61 received frame(size=0; request=1; stream=2; streamflags=; type=command-response; flags=eos)
53 62 response: b'Bk\xad\xa5\xc6u\x98\xcae\x03mW\xd9\xe4\xb6K\x0c\x1c\xe7\xa0'
54 63
55 64 $ cat error.log
@@ -1,91 +1,109
1 1 $ . $TESTDIR/wireprotohelpers.sh
2 2
3 3 $ hg init server
4 4 $ enablehttpv2 server
5 5 $ cd server
6 6 $ cat >> .hg/hgrc << EOF
7 7 > [web]
8 8 > push_ssl = false
9 9 > allow-push = *
10 10 > EOF
11 11 $ hg debugdrawdag << EOF
12 12 > C D
13 13 > |/
14 14 > B
15 15 > |
16 16 > A
17 17 > EOF
18 18
19 19 $ hg serve -p $HGPORT -d --pid-file hg.pid -E error.log
20 20 $ cat hg.pid > $DAEMON_PIDS
21 21
22 22 pushkey for a bookmark works
23 23
24 24 $ sendhttpv2peer << EOF
25 25 > command pushkey
26 26 > namespace bookmarks
27 27 > key @
28 28 > old
29 29 > new 426bada5c67598ca65036d57d9e4b64b0c1ce7a0
30 30 > EOF
31 31 creating http peer for wire protocol version 2
32 32 sending pushkey command
33 33 s> *\r\n (glob)
34 34 s> Accept-Encoding: identity\r\n
35 35 s> accept: application/mercurial-exp-framing-0005\r\n
36 36 s> content-type: application/mercurial-exp-framing-0005\r\n
37 37 s> content-length: 105\r\n
38 38 s> host: $LOCALIP:$HGPORT\r\n (glob)
39 39 s> user-agent: Mercurial debugwireproto\r\n
40 40 s> \r\n
41 41 s> a\x00\x00\x01\x00\x01\x01\x11\xa2Dargs\xa4CkeyA@InamespaceIbookmarksCnewX(426bada5c67598ca65036d57d9e4b64b0c1ce7a0Cold@DnameGpushkey
42 42 s> makefile('rb', None)
43 43 s> HTTP/1.1 200 OK\r\n
44 44 s> Server: testing stub value\r\n
45 45 s> Date: $HTTP_DATE$\r\n
46 46 s> Content-Type: application/mercurial-exp-framing-0005\r\n
47 47 s> Transfer-Encoding: chunked\r\n
48 48 s> \r\n
49 s> 14\r\n
50 s> \x0c\x00\x00\x01\x00\x02\x012
51 s> \xa1FstatusBok\xf5
49 s> 13\r\n
50 s> \x0b\x00\x00\x01\x00\x02\x011
51 s> \xa1FstatusBok
52 52 s> \r\n
53 received frame(size=12; request=1; stream=2; streamflags=stream-begin; type=command-response; flags=eos)
53 received frame(size=11; request=1; stream=2; streamflags=stream-begin; type=command-response; flags=continuation)
54 s> 9\r\n
55 s> \x01\x00\x00\x01\x00\x02\x001
56 s> \xf5
57 s> \r\n
58 received frame(size=1; request=1; stream=2; streamflags=; type=command-response; flags=continuation)
59 s> 8\r\n
60 s> \x00\x00\x00\x01\x00\x02\x002
61 s> \r\n
54 62 s> 0\r\n
55 63 s> \r\n
64 received frame(size=0; request=1; stream=2; streamflags=; type=command-response; flags=eos)
56 65 response: True
57 66
58 67 $ sendhttpv2peer << EOF
59 68 > command listkeys
60 69 > namespace bookmarks
61 70 > EOF
62 71 creating http peer for wire protocol version 2
63 72 sending listkeys command
64 73 s> POST /api/exp-http-v2-0001/ro/listkeys HTTP/1.1\r\n
65 74 s> Accept-Encoding: identity\r\n
66 75 s> accept: application/mercurial-exp-framing-0005\r\n
67 76 s> content-type: application/mercurial-exp-framing-0005\r\n
68 77 s> content-length: 49\r\n
69 78 s> host: $LOCALIP:$HGPORT\r\n (glob)
70 79 s> user-agent: Mercurial debugwireproto\r\n
71 80 s> \r\n
72 81 s> )\x00\x00\x01\x00\x01\x01\x11\xa2Dargs\xa1InamespaceIbookmarksDnameHlistkeys
73 82 s> makefile('rb', None)
74 83 s> HTTP/1.1 200 OK\r\n
75 84 s> Server: testing stub value\r\n
76 85 s> Date: $HTTP_DATE$\r\n
77 86 s> Content-Type: application/mercurial-exp-framing-0005\r\n
78 87 s> Transfer-Encoding: chunked\r\n
79 88 s> \r\n
80 s> 40\r\n
81 s> 8\x00\x00\x01\x00\x02\x012
82 s> \xa1FstatusBok\xa1A@X(426bada5c67598ca65036d57d9e4b64b0c1ce7a0
89 s> 13\r\n
90 s> \x0b\x00\x00\x01\x00\x02\x011
91 s> \xa1FstatusBok
83 92 s> \r\n
84 received frame(size=56; request=1; stream=2; streamflags=stream-begin; type=command-response; flags=eos)
93 received frame(size=11; request=1; stream=2; streamflags=stream-begin; type=command-response; flags=continuation)
94 s> 35\r\n
95 s> -\x00\x00\x01\x00\x02\x001
96 s> \xa1A@X(426bada5c67598ca65036d57d9e4b64b0c1ce7a0
97 s> \r\n
98 received frame(size=45; request=1; stream=2; streamflags=; type=command-response; flags=continuation)
99 s> 8\r\n
100 s> \x00\x00\x00\x01\x00\x02\x002
101 s> \r\n
85 102 s> 0\r\n
86 103 s> \r\n
104 received frame(size=0; request=1; stream=2; streamflags=; type=command-response; flags=eos)
87 105 response: {
88 106 b'@': b'426bada5c67598ca65036d57d9e4b64b0c1ce7a0'
89 107 }
90 108
91 109 $ cat error.log
@@ -1,58 +1,58
1 1 HTTPV2=exp-http-v2-0001
2 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 wireprotov1server,
20 20 wireprotov2server,
21 21 )
22 22
23 23 @wireprotov1server.wireprotocommand(b'customreadonly', permission=b'pull')
24 24 def customreadonlyv1(repo, proto):
25 25 return wireprototypes.bytesresponse(b'customreadonly bytes response')
26 26
27 27 @wireprotov2server.wireprotocommand(b'customreadonly', permission=b'pull')
28 28 def customreadonlyv2(repo, proto):
29 return wireprototypes.cborresponse(b'customreadonly bytes response')
29 yield b'customreadonly bytes response'
30 30
31 31 @wireprotov1server.wireprotocommand(b'customreadwrite', permission=b'push')
32 32 def customreadwrite(repo, proto):
33 33 return wireprototypes.bytesresponse(b'customreadwrite bytes response')
34 34
35 35 @wireprotov2server.wireprotocommand(b'customreadwrite', permission=b'push')
36 36 def customreadwritev2(repo, proto):
37 return wireprototypes.cborresponse(b'customreadwrite bytes response')
37 yield b'customreadwrite bytes response'
38 38 EOF
39 39
40 40 cat >> $HGRCPATH << EOF
41 41 [extensions]
42 42 drawdag = $TESTDIR/drawdag.py
43 43 EOF
44 44
45 45 enabledummycommands() {
46 46 cat >> $HGRCPATH << EOF
47 47 [extensions]
48 48 dummycommands = $TESTTMP/dummycommands.py
49 49 EOF
50 50 }
51 51
52 52 enablehttpv2() {
53 53 cat >> $1/.hg/hgrc << EOF
54 54 [experimental]
55 55 web.apiserver = true
56 56 web.api.http-v2 = true
57 57 EOF
58 58 }
General Comments 0
You need to be logged in to leave comments. Login now