Show More
@@ -0,0 +1,275 b'' | |||
|
1 | from __future__ import absolute_import, print_function | |
|
2 | ||
|
3 | import unittest | |
|
4 | ||
|
5 | from mercurial import ( | |
|
6 | util, | |
|
7 | wireprotoframing as framing, | |
|
8 | ) | |
|
9 | ||
|
10 | ffs = framing.makeframefromhumanstring | |
|
11 | ||
|
12 | def makereactor(): | |
|
13 | return framing.serverreactor() | |
|
14 | ||
|
15 | def sendframes(reactor, gen): | |
|
16 | """Send a generator of frame bytearray to a reactor. | |
|
17 | ||
|
18 | Emits a generator of results from ``onframerecv()`` calls. | |
|
19 | """ | |
|
20 | for frame in gen: | |
|
21 | frametype, frameflags, framelength = framing.parseheader(frame) | |
|
22 | payload = frame[framing.FRAME_HEADER_SIZE:] | |
|
23 | assert len(payload) == framelength | |
|
24 | ||
|
25 | yield reactor.onframerecv(frametype, frameflags, payload) | |
|
26 | ||
|
27 | def sendcommandframes(reactor, cmd, args, datafh=None): | |
|
28 | """Generate frames to run a command and send them to a reactor.""" | |
|
29 | return sendframes(reactor, framing.createcommandframes(cmd, args, datafh)) | |
|
30 | ||
|
31 | class FrameTests(unittest.TestCase): | |
|
32 | def testdataexactframesize(self): | |
|
33 | data = util.bytesio(b'x' * framing.DEFAULT_MAX_FRAME_SIZE) | |
|
34 | ||
|
35 | frames = list(framing.createcommandframes(b'command', {}, data)) | |
|
36 | self.assertEqual(frames, [ | |
|
37 | ffs(b'command-name have-data command'), | |
|
38 | ffs(b'command-data continuation %s' % data.getvalue()), | |
|
39 | ffs(b'command-data eos ') | |
|
40 | ]) | |
|
41 | ||
|
42 | def testdatamultipleframes(self): | |
|
43 | data = util.bytesio(b'x' * (framing.DEFAULT_MAX_FRAME_SIZE + 1)) | |
|
44 | frames = list(framing.createcommandframes(b'command', {}, data)) | |
|
45 | self.assertEqual(frames, [ | |
|
46 | ffs(b'command-name have-data command'), | |
|
47 | ffs(b'command-data continuation %s' % ( | |
|
48 | b'x' * framing.DEFAULT_MAX_FRAME_SIZE)), | |
|
49 | ffs(b'command-data eos x'), | |
|
50 | ]) | |
|
51 | ||
|
52 | def testargsanddata(self): | |
|
53 | data = util.bytesio(b'x' * 100) | |
|
54 | ||
|
55 | frames = list(framing.createcommandframes(b'command', { | |
|
56 | b'key1': b'key1value', | |
|
57 | b'key2': b'key2value', | |
|
58 | b'key3': b'key3value', | |
|
59 | }, data)) | |
|
60 | ||
|
61 | self.assertEqual(frames, [ | |
|
62 | ffs(b'command-name have-args|have-data command'), | |
|
63 | ffs(br'command-argument 0 \x04\x00\x09\x00key1key1value'), | |
|
64 | ffs(br'command-argument 0 \x04\x00\x09\x00key2key2value'), | |
|
65 | ffs(br'command-argument eoa \x04\x00\x09\x00key3key3value'), | |
|
66 | ffs(b'command-data eos %s' % data.getvalue()), | |
|
67 | ]) | |
|
68 | ||
|
69 | class ServerReactorTests(unittest.TestCase): | |
|
70 | def _sendsingleframe(self, reactor, s): | |
|
71 | results = list(sendframes(reactor, [ffs(s)])) | |
|
72 | self.assertEqual(len(results), 1) | |
|
73 | ||
|
74 | return results[0] | |
|
75 | ||
|
76 | def assertaction(self, res, expected): | |
|
77 | self.assertIsInstance(res, tuple) | |
|
78 | self.assertEqual(len(res), 2) | |
|
79 | self.assertIsInstance(res[1], dict) | |
|
80 | self.assertEqual(res[0], expected) | |
|
81 | ||
|
82 | def test1framecommand(self): | |
|
83 | """Receiving a command in a single frame yields request to run it.""" | |
|
84 | reactor = makereactor() | |
|
85 | results = list(sendcommandframes(reactor, b'mycommand', {})) | |
|
86 | self.assertEqual(len(results), 1) | |
|
87 | self.assertaction(results[0], 'runcommand') | |
|
88 | self.assertEqual(results[0][1], { | |
|
89 | 'command': b'mycommand', | |
|
90 | 'args': {}, | |
|
91 | 'data': None, | |
|
92 | }) | |
|
93 | ||
|
94 | def test1argument(self): | |
|
95 | reactor = makereactor() | |
|
96 | results = list(sendcommandframes(reactor, b'mycommand', | |
|
97 | {b'foo': b'bar'})) | |
|
98 | self.assertEqual(len(results), 2) | |
|
99 | self.assertaction(results[0], 'wantframe') | |
|
100 | self.assertaction(results[1], 'runcommand') | |
|
101 | self.assertEqual(results[1][1], { | |
|
102 | 'command': b'mycommand', | |
|
103 | 'args': {b'foo': b'bar'}, | |
|
104 | 'data': None, | |
|
105 | }) | |
|
106 | ||
|
107 | def testmultiarguments(self): | |
|
108 | reactor = makereactor() | |
|
109 | results = list(sendcommandframes(reactor, b'mycommand', | |
|
110 | {b'foo': b'bar', b'biz': b'baz'})) | |
|
111 | self.assertEqual(len(results), 3) | |
|
112 | self.assertaction(results[0], 'wantframe') | |
|
113 | self.assertaction(results[1], 'wantframe') | |
|
114 | self.assertaction(results[2], 'runcommand') | |
|
115 | self.assertEqual(results[2][1], { | |
|
116 | 'command': b'mycommand', | |
|
117 | 'args': {b'foo': b'bar', b'biz': b'baz'}, | |
|
118 | 'data': None, | |
|
119 | }) | |
|
120 | ||
|
121 | def testsimplecommanddata(self): | |
|
122 | reactor = makereactor() | |
|
123 | results = list(sendcommandframes(reactor, b'mycommand', {}, | |
|
124 | util.bytesio(b'data!'))) | |
|
125 | self.assertEqual(len(results), 2) | |
|
126 | self.assertaction(results[0], 'wantframe') | |
|
127 | self.assertaction(results[1], 'runcommand') | |
|
128 | self.assertEqual(results[1][1], { | |
|
129 | 'command': b'mycommand', | |
|
130 | 'args': {}, | |
|
131 | 'data': b'data!', | |
|
132 | }) | |
|
133 | ||
|
134 | def testmultipledataframes(self): | |
|
135 | frames = [ | |
|
136 | ffs(b'command-name have-data mycommand'), | |
|
137 | ffs(b'command-data continuation data1'), | |
|
138 | ffs(b'command-data continuation data2'), | |
|
139 | ffs(b'command-data eos data3'), | |
|
140 | ] | |
|
141 | ||
|
142 | reactor = makereactor() | |
|
143 | results = list(sendframes(reactor, frames)) | |
|
144 | self.assertEqual(len(results), 4) | |
|
145 | for i in range(3): | |
|
146 | self.assertaction(results[i], 'wantframe') | |
|
147 | self.assertaction(results[3], 'runcommand') | |
|
148 | self.assertEqual(results[3][1], { | |
|
149 | 'command': b'mycommand', | |
|
150 | 'args': {}, | |
|
151 | 'data': b'data1data2data3', | |
|
152 | }) | |
|
153 | ||
|
154 | def testargumentanddata(self): | |
|
155 | frames = [ | |
|
156 | ffs(b'command-name have-args|have-data command'), | |
|
157 | ffs(br'command-argument 0 \x03\x00\x03\x00keyval'), | |
|
158 | ffs(br'command-argument eoa \x03\x00\x03\x00foobar'), | |
|
159 | ffs(b'command-data continuation value1'), | |
|
160 | ffs(b'command-data eos value2'), | |
|
161 | ] | |
|
162 | ||
|
163 | reactor = makereactor() | |
|
164 | results = list(sendframes(reactor, frames)) | |
|
165 | ||
|
166 | self.assertaction(results[-1], 'runcommand') | |
|
167 | self.assertEqual(results[-1][1], { | |
|
168 | 'command': b'command', | |
|
169 | 'args': { | |
|
170 | b'key': b'val', | |
|
171 | b'foo': b'bar', | |
|
172 | }, | |
|
173 | 'data': b'value1value2', | |
|
174 | }) | |
|
175 | ||
|
176 | def testunexpectedcommandargument(self): | |
|
177 | """Command argument frame when not running a command is an error.""" | |
|
178 | result = self._sendsingleframe(makereactor(), | |
|
179 | b'command-argument 0 ignored') | |
|
180 | self.assertaction(result, 'error') | |
|
181 | self.assertEqual(result[1], { | |
|
182 | 'message': b'expected command frame; got 2', | |
|
183 | }) | |
|
184 | ||
|
185 | def testunexpectedcommanddata(self): | |
|
186 | """Command argument frame when not running a command is an error.""" | |
|
187 | result = self._sendsingleframe(makereactor(), | |
|
188 | b'command-data 0 ignored') | |
|
189 | self.assertaction(result, 'error') | |
|
190 | self.assertEqual(result[1], { | |
|
191 | 'message': b'expected command frame; got 3', | |
|
192 | }) | |
|
193 | ||
|
194 | def testmissingcommandframeflags(self): | |
|
195 | """Command name frame must have flags set.""" | |
|
196 | result = self._sendsingleframe(makereactor(), | |
|
197 | b'command-name 0 command') | |
|
198 | self.assertaction(result, 'error') | |
|
199 | self.assertEqual(result[1], { | |
|
200 | 'message': b'missing frame flags on command frame', | |
|
201 | }) | |
|
202 | ||
|
203 | def testmissingargumentframe(self): | |
|
204 | frames = [ | |
|
205 | ffs(b'command-name have-args command'), | |
|
206 | ffs(b'command-name 0 ignored'), | |
|
207 | ] | |
|
208 | ||
|
209 | results = list(sendframes(makereactor(), frames)) | |
|
210 | self.assertEqual(len(results), 2) | |
|
211 | self.assertaction(results[0], 'wantframe') | |
|
212 | self.assertaction(results[1], 'error') | |
|
213 | self.assertEqual(results[1][1], { | |
|
214 | 'message': b'expected command argument frame; got 1', | |
|
215 | }) | |
|
216 | ||
|
217 | def testincompleteargumentname(self): | |
|
218 | """Argument frame with incomplete name.""" | |
|
219 | frames = [ | |
|
220 | ffs(b'command-name have-args command1'), | |
|
221 | ffs(br'command-argument eoa \x04\x00\xde\xadfoo'), | |
|
222 | ] | |
|
223 | ||
|
224 | results = list(sendframes(makereactor(), frames)) | |
|
225 | self.assertEqual(len(results), 2) | |
|
226 | self.assertaction(results[0], 'wantframe') | |
|
227 | self.assertaction(results[1], 'error') | |
|
228 | self.assertEqual(results[1][1], { | |
|
229 | 'message': b'malformed argument frame: partial argument name', | |
|
230 | }) | |
|
231 | ||
|
232 | def testincompleteargumentvalue(self): | |
|
233 | """Argument frame with incomplete value.""" | |
|
234 | frames = [ | |
|
235 | ffs(b'command-name have-args command'), | |
|
236 | ffs(br'command-argument eoa \x03\x00\xaa\xaafoopartialvalue'), | |
|
237 | ] | |
|
238 | ||
|
239 | results = list(sendframes(makereactor(), frames)) | |
|
240 | self.assertEqual(len(results), 2) | |
|
241 | self.assertaction(results[0], 'wantframe') | |
|
242 | self.assertaction(results[1], 'error') | |
|
243 | self.assertEqual(results[1][1], { | |
|
244 | 'message': b'malformed argument frame: partial argument value', | |
|
245 | }) | |
|
246 | ||
|
247 | def testmissingcommanddataframe(self): | |
|
248 | frames = [ | |
|
249 | ffs(b'command-name have-data command1'), | |
|
250 | ffs(b'command-name eos command2'), | |
|
251 | ] | |
|
252 | results = list(sendframes(makereactor(), frames)) | |
|
253 | self.assertEqual(len(results), 2) | |
|
254 | self.assertaction(results[0], 'wantframe') | |
|
255 | self.assertaction(results[1], 'error') | |
|
256 | self.assertEqual(results[1][1], { | |
|
257 | 'message': b'expected command data frame; got 1', | |
|
258 | }) | |
|
259 | ||
|
260 | def testmissingcommanddataframeflags(self): | |
|
261 | frames = [ | |
|
262 | ffs(b'command-name have-data command1'), | |
|
263 | ffs(b'command-data 0 data'), | |
|
264 | ] | |
|
265 | results = list(sendframes(makereactor(), frames)) | |
|
266 | self.assertEqual(len(results), 2) | |
|
267 | self.assertaction(results[0], 'wantframe') | |
|
268 | self.assertaction(results[1], 'error') | |
|
269 | self.assertEqual(results[1][1], { | |
|
270 | 'message': b'command data frame without flags', | |
|
271 | }) | |
|
272 | ||
|
273 | if __name__ == '__main__': | |
|
274 | import silenttestrunner | |
|
275 | silenttestrunner.main(__name__) |
@@ -586,6 +586,9 b" coreconfigitem('experimental', 'web.apis" | |||
|
586 | 586 | coreconfigitem('experimental', 'web.api.http-v2', |
|
587 | 587 | default=False, |
|
588 | 588 | ) |
|
589 | coreconfigitem('experimental', 'web.api.debugreflect', | |
|
590 | default=False, | |
|
591 | ) | |
|
589 | 592 | coreconfigitem('experimental', 'xdiff', |
|
590 | 593 | default=False, |
|
591 | 594 | ) |
@@ -2564,6 +2564,14 b' class cappedreader(object):' | |||
|
2564 | 2564 | |
|
2565 | 2565 | return data |
|
2566 | 2566 | |
|
2567 | def readinto(self, b): | |
|
2568 | res = self.read(len(b)) | |
|
2569 | if res is None: | |
|
2570 | return None | |
|
2571 | ||
|
2572 | b[0:len(res)] = res | |
|
2573 | return len(res) | |
|
2574 | ||
|
2567 | 2575 | def stringmatcher(pattern, casesensitive=True): |
|
2568 | 2576 | """ |
|
2569 | 2577 | accepts a string, possibly starting with 're:' or 'literal:' prefix. |
@@ -13,7 +13,9 b' from __future__ import absolute_import' | |||
|
13 | 13 | |
|
14 | 14 | import struct |
|
15 | 15 | |
|
16 | from .i18n import _ | |
|
16 | 17 | from . import ( |
|
18 | error, | |
|
17 | 19 | util, |
|
18 | 20 | ) |
|
19 | 21 | |
@@ -105,6 +107,51 b' def makeframefromhumanstring(s):' | |||
|
105 | 107 | |
|
106 | 108 | return makeframe(frametype, finalflags, payload) |
|
107 | 109 | |
|
110 | def parseheader(data): | |
|
111 | """Parse a unified framing protocol frame header from a buffer. | |
|
112 | ||
|
113 | The header is expected to be in the buffer at offset 0 and the | |
|
114 | buffer is expected to be large enough to hold a full header. | |
|
115 | """ | |
|
116 | # 24 bits payload length (little endian) | |
|
117 | # 4 bits frame type | |
|
118 | # 4 bits frame flags | |
|
119 | # ... payload | |
|
120 | framelength = data[0] + 256 * data[1] + 16384 * data[2] | |
|
121 | typeflags = data[3] | |
|
122 | ||
|
123 | frametype = (typeflags & 0xf0) >> 4 | |
|
124 | frameflags = typeflags & 0x0f | |
|
125 | ||
|
126 | return frametype, frameflags, framelength | |
|
127 | ||
|
128 | def readframe(fh): | |
|
129 | """Read a unified framing protocol frame from a file object. | |
|
130 | ||
|
131 | Returns a 3-tuple of (type, flags, payload) for the decoded frame or | |
|
132 | None if no frame is available. May raise if a malformed frame is | |
|
133 | seen. | |
|
134 | """ | |
|
135 | header = bytearray(FRAME_HEADER_SIZE) | |
|
136 | ||
|
137 | readcount = fh.readinto(header) | |
|
138 | ||
|
139 | if readcount == 0: | |
|
140 | return None | |
|
141 | ||
|
142 | if readcount != FRAME_HEADER_SIZE: | |
|
143 | raise error.Abort(_('received incomplete frame: got %d bytes: %s') % | |
|
144 | (readcount, header)) | |
|
145 | ||
|
146 | frametype, frameflags, framelength = parseheader(header) | |
|
147 | ||
|
148 | payload = fh.read(framelength) | |
|
149 | if len(payload) != framelength: | |
|
150 | raise error.Abort(_('frame length error: expected %d; got %d') % | |
|
151 | (framelength, len(payload))) | |
|
152 | ||
|
153 | return frametype, frameflags, payload | |
|
154 | ||
|
108 | 155 | def createcommandframes(cmd, args, datafh=None): |
|
109 | 156 | """Create frames necessary to transmit a request to run a command. |
|
110 | 157 | |
@@ -154,3 +201,195 b' def createcommandframes(cmd, args, dataf' | |||
|
154 | 201 | |
|
155 | 202 | if done: |
|
156 | 203 | break |
|
204 | ||
|
205 | class serverreactor(object): | |
|
206 | """Holds state of a server handling frame-based protocol requests. | |
|
207 | ||
|
208 | This class is the "brain" of the unified frame-based protocol server | |
|
209 | component. While the protocol is stateless from the perspective of | |
|
210 | requests/commands, something needs to track which frames have been | |
|
211 | received, what frames to expect, etc. This class is that thing. | |
|
212 | ||
|
213 | Instances are modeled as a state machine of sorts. Instances are also | |
|
214 | reactionary to external events. The point of this class is to encapsulate | |
|
215 | the state of the connection and the exchange of frames, not to perform | |
|
216 | work. Instead, callers tell this class when something occurs, like a | |
|
217 | frame arriving. If that activity is worthy of a follow-up action (say | |
|
218 | *run a command*), the return value of that handler will say so. | |
|
219 | ||
|
220 | I/O and CPU intensive operations are purposefully delegated outside of | |
|
221 | this class. | |
|
222 | ||
|
223 | Consumers are expected to tell instances when events occur. They do so by | |
|
224 | calling the various ``on*`` methods. These methods return a 2-tuple | |
|
225 | describing any follow-up action(s) to take. The first element is the | |
|
226 | name of an action to perform. The second is a data structure (usually | |
|
227 | a dict) specific to that action that contains more information. e.g. | |
|
228 | if the server wants to send frames back to the client, the data structure | |
|
229 | will contain a reference to those frames. | |
|
230 | ||
|
231 | Valid actions that consumers can be instructed to take are: | |
|
232 | ||
|
233 | error | |
|
234 | Indicates that an error occurred. Consumer should probably abort. | |
|
235 | ||
|
236 | runcommand | |
|
237 | Indicates that the consumer should run a wire protocol command. Details | |
|
238 | of the command to run are given in the data structure. | |
|
239 | ||
|
240 | wantframe | |
|
241 | Indicates that nothing of interest happened and the server is waiting on | |
|
242 | more frames from the client before anything interesting can be done. | |
|
243 | """ | |
|
244 | ||
|
245 | def __init__(self): | |
|
246 | self._state = 'idle' | |
|
247 | self._activecommand = None | |
|
248 | self._activeargs = None | |
|
249 | self._activedata = None | |
|
250 | self._expectingargs = None | |
|
251 | self._expectingdata = None | |
|
252 | self._activeargname = None | |
|
253 | self._activeargchunks = None | |
|
254 | ||
|
255 | def onframerecv(self, frametype, frameflags, payload): | |
|
256 | """Process a frame that has been received off the wire. | |
|
257 | ||
|
258 | Returns a dict with an ``action`` key that details what action, | |
|
259 | if any, the consumer should take next. | |
|
260 | """ | |
|
261 | handlers = { | |
|
262 | 'idle': self._onframeidle, | |
|
263 | 'command-receiving-args': self._onframereceivingargs, | |
|
264 | 'command-receiving-data': self._onframereceivingdata, | |
|
265 | 'errored': self._onframeerrored, | |
|
266 | } | |
|
267 | ||
|
268 | meth = handlers.get(self._state) | |
|
269 | if not meth: | |
|
270 | raise error.ProgrammingError('unhandled state: %s' % self._state) | |
|
271 | ||
|
272 | return meth(frametype, frameflags, payload) | |
|
273 | ||
|
274 | def _makeerrorresult(self, msg): | |
|
275 | return 'error', { | |
|
276 | 'message': msg, | |
|
277 | } | |
|
278 | ||
|
279 | def _makeruncommandresult(self): | |
|
280 | return 'runcommand', { | |
|
281 | 'command': self._activecommand, | |
|
282 | 'args': self._activeargs, | |
|
283 | 'data': self._activedata.getvalue() if self._activedata else None, | |
|
284 | } | |
|
285 | ||
|
286 | def _makewantframeresult(self): | |
|
287 | return 'wantframe', { | |
|
288 | 'state': self._state, | |
|
289 | } | |
|
290 | ||
|
291 | def _onframeidle(self, frametype, frameflags, payload): | |
|
292 | # The only frame type that should be received in this state is a | |
|
293 | # command request. | |
|
294 | if frametype != FRAME_TYPE_COMMAND_NAME: | |
|
295 | self._state = 'errored' | |
|
296 | return self._makeerrorresult( | |
|
297 | _('expected command frame; got %d') % frametype) | |
|
298 | ||
|
299 | self._activecommand = payload | |
|
300 | self._activeargs = {} | |
|
301 | self._activedata = None | |
|
302 | ||
|
303 | if frameflags & FLAG_COMMAND_NAME_EOS: | |
|
304 | return self._makeruncommandresult() | |
|
305 | ||
|
306 | self._expectingargs = bool(frameflags & FLAG_COMMAND_NAME_HAVE_ARGS) | |
|
307 | self._expectingdata = bool(frameflags & FLAG_COMMAND_NAME_HAVE_DATA) | |
|
308 | ||
|
309 | if self._expectingargs: | |
|
310 | self._state = 'command-receiving-args' | |
|
311 | return self._makewantframeresult() | |
|
312 | elif self._expectingdata: | |
|
313 | self._activedata = util.bytesio() | |
|
314 | self._state = 'command-receiving-data' | |
|
315 | return self._makewantframeresult() | |
|
316 | else: | |
|
317 | self._state = 'errored' | |
|
318 | return self._makeerrorresult(_('missing frame flags on ' | |
|
319 | 'command frame')) | |
|
320 | ||
|
321 | def _onframereceivingargs(self, frametype, frameflags, payload): | |
|
322 | if frametype != FRAME_TYPE_COMMAND_ARGUMENT: | |
|
323 | self._state = 'errored' | |
|
324 | return self._makeerrorresult(_('expected command argument ' | |
|
325 | 'frame; got %d') % frametype) | |
|
326 | ||
|
327 | offset = 0 | |
|
328 | namesize, valuesize = ARGUMENT_FRAME_HEADER.unpack_from(payload) | |
|
329 | offset += ARGUMENT_FRAME_HEADER.size | |
|
330 | ||
|
331 | # The argument name MUST fit inside the frame. | |
|
332 | argname = bytes(payload[offset:offset + namesize]) | |
|
333 | offset += namesize | |
|
334 | ||
|
335 | if len(argname) != namesize: | |
|
336 | self._state = 'errored' | |
|
337 | return self._makeerrorresult(_('malformed argument frame: ' | |
|
338 | 'partial argument name')) | |
|
339 | ||
|
340 | argvalue = bytes(payload[offset:]) | |
|
341 | ||
|
342 | # Argument value spans multiple frames. Record our active state | |
|
343 | # and wait for the next frame. | |
|
344 | if frameflags & FLAG_COMMAND_ARGUMENT_CONTINUATION: | |
|
345 | raise error.ProgrammingError('not yet implemented') | |
|
346 | self._activeargname = argname | |
|
347 | self._activeargchunks = [argvalue] | |
|
348 | self._state = 'command-arg-continuation' | |
|
349 | return self._makewantframeresult() | |
|
350 | ||
|
351 | # Common case: the argument value is completely contained in this | |
|
352 | # frame. | |
|
353 | ||
|
354 | if len(argvalue) != valuesize: | |
|
355 | self._state = 'errored' | |
|
356 | return self._makeerrorresult(_('malformed argument frame: ' | |
|
357 | 'partial argument value')) | |
|
358 | ||
|
359 | self._activeargs[argname] = argvalue | |
|
360 | ||
|
361 | if frameflags & FLAG_COMMAND_ARGUMENT_EOA: | |
|
362 | if self._expectingdata: | |
|
363 | self._state = 'command-receiving-data' | |
|
364 | self._activedata = util.bytesio() | |
|
365 | # TODO signal request to run a command once we don't | |
|
366 | # buffer data frames. | |
|
367 | return self._makewantframeresult() | |
|
368 | else: | |
|
369 | self._state = 'waiting' | |
|
370 | return self._makeruncommandresult() | |
|
371 | else: | |
|
372 | return self._makewantframeresult() | |
|
373 | ||
|
374 | def _onframereceivingdata(self, frametype, frameflags, payload): | |
|
375 | if frametype != FRAME_TYPE_COMMAND_DATA: | |
|
376 | self._state = 'errored' | |
|
377 | return self._makeerrorresult(_('expected command data frame; ' | |
|
378 | 'got %d') % frametype) | |
|
379 | ||
|
380 | # TODO support streaming data instead of buffering it. | |
|
381 | self._activedata.write(payload) | |
|
382 | ||
|
383 | if frameflags & FLAG_COMMAND_DATA_CONTINUATION: | |
|
384 | return self._makewantframeresult() | |
|
385 | elif frameflags & FLAG_COMMAND_DATA_EOS: | |
|
386 | self._activedata.seek(0) | |
|
387 | self._state = 'idle' | |
|
388 | return self._makeruncommandresult() | |
|
389 | else: | |
|
390 | self._state = 'errored' | |
|
391 | return self._makeerrorresult(_('command data frame without ' | |
|
392 | 'flags')) | |
|
393 | ||
|
394 | def _onframeerrored(self, frametype, frameflags, payload): | |
|
395 | return self._makeerrorresult(_('server already errored')) |
@@ -19,6 +19,7 b' from . import (' | |||
|
19 | 19 | pycompat, |
|
20 | 20 | util, |
|
21 | 21 | wireproto, |
|
22 | wireprotoframing, | |
|
22 | 23 | wireprototypes, |
|
23 | 24 | ) |
|
24 | 25 | |
@@ -319,6 +320,11 b' def _handlehttpv2request(rctx, req, res,' | |||
|
319 | 320 | res.setbodybytes('permission denied') |
|
320 | 321 | return |
|
321 | 322 | |
|
323 | # We have a special endpoint to reflect the request back at the client. | |
|
324 | if command == b'debugreflect': | |
|
325 | _processhttpv2reflectrequest(rctx.repo.ui, rctx.repo, req, res) | |
|
326 | return | |
|
327 | ||
|
322 | 328 | if command not in wireproto.commands: |
|
323 | 329 | res.status = b'404 Not Found' |
|
324 | 330 | res.headers[b'Content-Type'] = b'text/plain' |
@@ -343,8 +349,7 b' def _handlehttpv2request(rctx, req, res,' | |||
|
343 | 349 | % FRAMINGTYPE) |
|
344 | 350 | return |
|
345 | 351 | |
|
346 | if (b'Content-Type' in req.headers | |
|
347 | and req.headers[b'Content-Type'] != FRAMINGTYPE): | |
|
352 | if req.headers.get(b'Content-Type') != FRAMINGTYPE: | |
|
348 | 353 | res.status = b'415 Unsupported Media Type' |
|
349 | 354 | # TODO we should send a response with appropriate media type, |
|
350 | 355 | # since client does Accept it. |
@@ -358,6 +363,49 b' def _handlehttpv2request(rctx, req, res,' | |||
|
358 | 363 | res.headers[b'Content-Type'] = b'text/plain' |
|
359 | 364 | res.setbodybytes(b'/'.join(urlparts) + b'\n') |
|
360 | 365 | |
|
366 | def _processhttpv2reflectrequest(ui, repo, req, res): | |
|
367 | """Reads unified frame protocol request and dumps out state to client. | |
|
368 | ||
|
369 | This special endpoint can be used to help debug the wire protocol. | |
|
370 | ||
|
371 | Instead of routing the request through the normal dispatch mechanism, | |
|
372 | we instead read all frames, decode them, and feed them into our state | |
|
373 | tracker. We then dump the log of all that activity back out to the | |
|
374 | client. | |
|
375 | """ | |
|
376 | import json | |
|
377 | ||
|
378 | # Reflection APIs have a history of being abused, accidentally disclosing | |
|
379 | # sensitive data, etc. So we have a config knob. | |
|
380 | if not ui.configbool('experimental', 'web.api.debugreflect'): | |
|
381 | res.status = b'404 Not Found' | |
|
382 | res.headers[b'Content-Type'] = b'text/plain' | |
|
383 | res.setbodybytes(_('debugreflect service not available')) | |
|
384 | return | |
|
385 | ||
|
386 | # We assume we have a unified framing protocol request body. | |
|
387 | ||
|
388 | reactor = wireprotoframing.serverreactor() | |
|
389 | states = [] | |
|
390 | ||
|
391 | while True: | |
|
392 | frame = wireprotoframing.readframe(req.bodyfh) | |
|
393 | ||
|
394 | if not frame: | |
|
395 | states.append(b'received: <no frame>') | |
|
396 | break | |
|
397 | ||
|
398 | frametype, frameflags, payload = frame | |
|
399 | states.append(b'received: %d %d %s' % (frametype, frameflags, payload)) | |
|
400 | ||
|
401 | action, meta = reactor.onframerecv(frametype, frameflags, payload) | |
|
402 | states.append(json.dumps((action, meta), sort_keys=True, | |
|
403 | separators=(', ', ': '))) | |
|
404 | ||
|
405 | res.status = b'200 OK' | |
|
406 | res.headers[b'Content-Type'] = b'text/plain' | |
|
407 | res.setbodybytes(b'\n'.join(states)) | |
|
408 | ||
|
361 | 409 | # Maps API name to metadata so custom API can be registered. |
|
362 | 410 | API_HANDLERS = { |
|
363 | 411 | HTTPV2: { |
@@ -276,7 +276,7 b' Restart server to allow non-ssl read-wri' | |||
|
276 | 276 | > allow-push = * |
|
277 | 277 | > EOF |
|
278 | 278 | |
|
279 | $ hg -R server serve -p $HGPORT -d --pid-file hg.pid | |
|
279 | $ hg -R server serve -p $HGPORT -d --pid-file hg.pid -E error.log | |
|
280 | 280 | $ cat hg.pid > $DAEMON_PIDS |
|
281 | 281 | |
|
282 | 282 | Authorized request for valid read-write command works |
@@ -329,3 +329,78 b' Authorized request for unknown command i' | |||
|
329 | 329 | s> Content-Length: 42\r\n |
|
330 | 330 | s> \r\n |
|
331 | 331 | s> unknown wire protocol command: badcommand\n |
|
332 | ||
|
333 | debugreflect isn't enabled by default | |
|
334 | ||
|
335 | $ send << EOF | |
|
336 | > httprequest POST api/$HTTPV2/ro/debugreflect | |
|
337 | > user-agent: test | |
|
338 | > EOF | |
|
339 | using raw connection to peer | |
|
340 | s> POST /api/exp-http-v2-0001/ro/debugreflect HTTP/1.1\r\n | |
|
341 | s> Accept-Encoding: identity\r\n | |
|
342 | s> user-agent: test\r\n | |
|
343 | s> host: $LOCALIP:$HGPORT\r\n (glob) | |
|
344 | s> \r\n | |
|
345 | s> makefile('rb', None) | |
|
346 | s> HTTP/1.1 404 Not Found\r\n | |
|
347 | s> Server: testing stub value\r\n | |
|
348 | s> Date: $HTTP_DATE$\r\n | |
|
349 | s> Content-Type: text/plain\r\n | |
|
350 | s> Content-Length: 34\r\n | |
|
351 | s> \r\n | |
|
352 | s> debugreflect service not available | |
|
353 | ||
|
354 | Restart server to get debugreflect endpoint | |
|
355 | ||
|
356 | $ killdaemons.py | |
|
357 | $ cat > server/.hg/hgrc << EOF | |
|
358 | > [experimental] | |
|
359 | > web.apiserver = true | |
|
360 | > web.api.debugreflect = true | |
|
361 | > web.api.http-v2 = true | |
|
362 | > [web] | |
|
363 | > push_ssl = false | |
|
364 | > allow-push = * | |
|
365 | > EOF | |
|
366 | ||
|
367 | $ hg -R server serve -p $HGPORT -d --pid-file hg.pid -E error.log | |
|
368 | $ cat hg.pid > $DAEMON_PIDS | |
|
369 | ||
|
370 | Command frames can be reflected via debugreflect | |
|
371 | ||
|
372 | $ send << EOF | |
|
373 | > httprequest POST api/$HTTPV2/ro/debugreflect | |
|
374 | > accept: $MEDIATYPE | |
|
375 | > content-type: $MEDIATYPE | |
|
376 | > user-agent: test | |
|
377 | > frame command-name have-args command1 | |
|
378 | > frame command-argument 0 \x03\x00\x04\x00fooval1 | |
|
379 | > frame command-argument eoa \x04\x00\x03\x00bar1val | |
|
380 | > EOF | |
|
381 | using raw connection to peer | |
|
382 | s> POST /api/exp-http-v2-0001/ro/debugreflect HTTP/1.1\r\n | |
|
383 | s> Accept-Encoding: identity\r\n | |
|
384 | s> accept: application/mercurial-exp-framing-0001\r\n | |
|
385 | s> content-type: application/mercurial-exp-framing-0001\r\n | |
|
386 | s> user-agent: test\r\n | |
|
387 | s> content-length: 42\r\n | |
|
388 | s> host: $LOCALIP:$HGPORT\r\n (glob) | |
|
389 | s> \r\n | |
|
390 | s> \x08\x00\x00\x12command1\x0b\x00\x00 \x03\x00\x04\x00fooval1\x0b\x00\x00"\x04\x00\x03\x00bar1val | |
|
391 | s> makefile('rb', None) | |
|
392 | s> HTTP/1.1 200 OK\r\n | |
|
393 | s> Server: testing stub value\r\n | |
|
394 | s> Date: $HTTP_DATE$\r\n | |
|
395 | s> Content-Type: text/plain\r\n | |
|
396 | s> Content-Length: 291\r\n | |
|
397 | s> \r\n | |
|
398 | s> received: 1 2 command1\n | |
|
399 | s> ["wantframe", {"state": "command-receiving-args"}]\n | |
|
400 | s> received: 2 0 \x03\x00\x04\x00fooval1\n | |
|
401 | s> ["wantframe", {"state": "command-receiving-args"}]\n | |
|
402 | s> received: 2 2 \x04\x00\x03\x00bar1val\n | |
|
403 | s> ["runcommand", {"args": {"bar1": "val", "foo": "val1"}, "command": "command1", "data": null}]\n | |
|
404 | s> received: <no frame> | |
|
405 | ||
|
406 | $ cat error.log |
General Comments 0
You need to be logged in to leave comments.
Login now