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