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