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 | coreconfigitem('experimental', 'web.api.http-v2', |
|
586 | coreconfigitem('experimental', 'web.api.http-v2', | |
587 | default=False, |
|
587 | default=False, | |
588 | ) |
|
588 | ) | |
|
589 | coreconfigitem('experimental', 'web.api.debugreflect', | |||
|
590 | default=False, | |||
|
591 | ) | |||
589 | coreconfigitem('experimental', 'xdiff', |
|
592 | coreconfigitem('experimental', 'xdiff', | |
590 | default=False, |
|
593 | default=False, | |
591 | ) |
|
594 | ) |
@@ -2564,6 +2564,14 b' class cappedreader(object):' | |||||
2564 |
|
2564 | |||
2565 | return data |
|
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 | def stringmatcher(pattern, casesensitive=True): |
|
2575 | def stringmatcher(pattern, casesensitive=True): | |
2568 | """ |
|
2576 | """ | |
2569 | accepts a string, possibly starting with 're:' or 'literal:' prefix. |
|
2577 | accepts a string, possibly starting with 're:' or 'literal:' prefix. |
@@ -13,7 +13,9 b' from __future__ import absolute_import' | |||||
13 |
|
13 | |||
14 | import struct |
|
14 | import struct | |
15 |
|
15 | |||
|
16 | from .i18n import _ | |||
16 | from . import ( |
|
17 | from . import ( | |
|
18 | error, | |||
17 | util, |
|
19 | util, | |
18 | ) |
|
20 | ) | |
19 |
|
21 | |||
@@ -105,6 +107,51 b' def makeframefromhumanstring(s):' | |||||
105 |
|
107 | |||
106 | return makeframe(frametype, finalflags, payload) |
|
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 | def createcommandframes(cmd, args, datafh=None): |
|
155 | def createcommandframes(cmd, args, datafh=None): | |
109 | """Create frames necessary to transmit a request to run a command. |
|
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 | if done: |
|
202 | if done: | |
156 | break |
|
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 | pycompat, |
|
19 | pycompat, | |
20 | util, |
|
20 | util, | |
21 | wireproto, |
|
21 | wireproto, | |
|
22 | wireprotoframing, | |||
22 | wireprototypes, |
|
23 | wireprototypes, | |
23 | ) |
|
24 | ) | |
24 |
|
25 | |||
@@ -319,6 +320,11 b' def _handlehttpv2request(rctx, req, res,' | |||||
319 | res.setbodybytes('permission denied') |
|
320 | res.setbodybytes('permission denied') | |
320 | return |
|
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 | if command not in wireproto.commands: |
|
328 | if command not in wireproto.commands: | |
323 | res.status = b'404 Not Found' |
|
329 | res.status = b'404 Not Found' | |
324 | res.headers[b'Content-Type'] = b'text/plain' |
|
330 | res.headers[b'Content-Type'] = b'text/plain' | |
@@ -343,8 +349,7 b' def _handlehttpv2request(rctx, req, res,' | |||||
343 | % FRAMINGTYPE) |
|
349 | % FRAMINGTYPE) | |
344 | return |
|
350 | return | |
345 |
|
351 | |||
346 | if (b'Content-Type' in req.headers |
|
352 | if req.headers.get(b'Content-Type') != FRAMINGTYPE: | |
347 | and req.headers[b'Content-Type'] != FRAMINGTYPE): |
|
|||
348 | res.status = b'415 Unsupported Media Type' |
|
353 | res.status = b'415 Unsupported Media Type' | |
349 | # TODO we should send a response with appropriate media type, |
|
354 | # TODO we should send a response with appropriate media type, | |
350 | # since client does Accept it. |
|
355 | # since client does Accept it. | |
@@ -358,6 +363,49 b' def _handlehttpv2request(rctx, req, res,' | |||||
358 | res.headers[b'Content-Type'] = b'text/plain' |
|
363 | res.headers[b'Content-Type'] = b'text/plain' | |
359 | res.setbodybytes(b'/'.join(urlparts) + b'\n') |
|
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 | # Maps API name to metadata so custom API can be registered. |
|
409 | # Maps API name to metadata so custom API can be registered. | |
362 | API_HANDLERS = { |
|
410 | API_HANDLERS = { | |
363 | HTTPV2: { |
|
411 | HTTPV2: { |
@@ -276,7 +276,7 b' Restart server to allow non-ssl read-wri' | |||||
276 | > allow-push = * |
|
276 | > allow-push = * | |
277 | > EOF |
|
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 | $ cat hg.pid > $DAEMON_PIDS |
|
280 | $ cat hg.pid > $DAEMON_PIDS | |
281 |
|
281 | |||
282 | Authorized request for valid read-write command works |
|
282 | Authorized request for valid read-write command works | |
@@ -329,3 +329,78 b' Authorized request for unknown command i' | |||||
329 | s> Content-Length: 42\r\n |
|
329 | s> Content-Length: 42\r\n | |
330 | s> \r\n |
|
330 | s> \r\n | |
331 | s> unknown wire protocol command: badcommand\n |
|
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