Merge ~pappacena/turnip:enable-http-v2 into turnip:master
- Git
- lp:~pappacena/turnip
- enable-http-v2
- Merge into master
Proposed by
Thiago F. Pappacena
Status: | Work in progress |
---|---|
Proposed branch: | ~pappacena/turnip:enable-http-v2 |
Merge into: | turnip:master |
Prerequisite: | ~pappacena/turnip:run-v2-commands |
Diff against target: |
684 lines (+166/-217) 7 files modified
turnip/pack/git.py (+20/-58) turnip/pack/helpers.py (+30/-63) turnip/pack/http.py (+27/-7) turnip/pack/tests/test_functional.py (+8/-5) turnip/pack/tests/test_git.py (+4/-56) turnip/pack/tests/test_helpers.py (+41/-28) turnip/pack/tests/test_http.py (+36/-0) |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Launchpad code reviewers | Pending | ||
Review via email: mp+389131@code.launchpad.net |
Commit message
Enabling protocol v2 compatibility on HTTP frontend
Description of the change
To post a comment you must log in.
~pappacena/turnip:enable-http-v2
updated
- 6bd0254... by Thiago F. Pappacena
-
Cleaning up tests
Unmerged commits
- 6bd0254... by Thiago F. Pappacena
-
Cleaning up tests
Preview Diff
[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1 | diff --git a/turnip/pack/git.py b/turnip/pack/git.py |
2 | index 1b34ac0..85fa57e 100644 |
3 | --- a/turnip/pack/git.py |
4 | +++ b/turnip/pack/git.py |
5 | @@ -31,14 +31,11 @@ from turnip.config import config |
6 | from turnip.helpers import compose_path |
7 | from turnip.pack.helpers import ( |
8 | decode_packet, |
9 | - DELIM_PKT, |
10 | decode_request, |
11 | encode_packet, |
12 | encode_request, |
13 | ensure_config, |
14 | ensure_hooks, |
15 | - FLUSH_PKT, |
16 | - get_capabilities_advertisement, |
17 | INCOMPLETE_PKT, |
18 | translate_xmlrpc_fault, |
19 | ) |
20 | @@ -242,20 +239,15 @@ class GitProcessProtocol(protocol.ProcessProtocol): |
21 | |
22 | _err_buffer = b'' |
23 | |
24 | - def __init__(self, peer, cmd_input=None): |
25 | + def __init__(self, peer): |
26 | self.peer = peer |
27 | - self.cmd_input = cmd_input |
28 | self.out_started = False |
29 | |
30 | def connectionMade(self): |
31 | self.peer.setPeer(self) |
32 | self.peer.transport.registerProducer(self, True) |
33 | - if not self.cmd_input: |
34 | - self.transport.registerProducer( |
35 | - UnstoppableProducerWrapper(self.peer.transport), True) |
36 | - else: |
37 | - self.transport.write(self.cmd_input) |
38 | - self.loseWriteConnection() |
39 | + self.transport.registerProducer( |
40 | + UnstoppableProducerWrapper(self.peer.transport), True) |
41 | self.peer.resumeProducing() |
42 | |
43 | def outReceived(self, data): |
44 | @@ -440,29 +432,12 @@ class PackBackendProtocol(PackServerProtocol): |
45 | hookrpc_key = None |
46 | expect_set_symbolic_ref = False |
47 | |
48 | - def getV2CommandInput(self, params): |
49 | - """Reconstruct what should be sent to git's stdin from the |
50 | - parameters received.""" |
51 | - cmd_input = encode_packet(b"command=%s\n" % params.get(b'command')) |
52 | - for capability in params["capabilities"].split(b"\n"): |
53 | - cmd_input += encode_packet(b"%s\n" % capability) |
54 | - cmd_input += DELIM_PKT |
55 | - ignore_keys = (b'capabilities', b'version') |
56 | - for k, v in params.items(): |
57 | - k = six.ensure_binary(k) |
58 | - if k.startswith(b"turnip-") or k in ignore_keys: |
59 | - continue |
60 | - for param_value in v.split(b'\n'): |
61 | - value = (b"" if not param_value else b" %s" % param_value) |
62 | - cmd_input += encode_packet(b"%s%s\n" % (k, value)) |
63 | - cmd_input += FLUSH_PKT |
64 | - return cmd_input |
65 | - |
66 | @defer.inlineCallbacks |
67 | def requestReceived(self, command, raw_pathname, params): |
68 | self.extractRequestMeta(command, raw_pathname, params) |
69 | self.command = command |
70 | self.raw_pathname = raw_pathname |
71 | + self.params = params |
72 | self.path = compose_path(self.factory.root, self.raw_pathname) |
73 | auth_params = self.createAuthParams(params) |
74 | |
75 | @@ -482,31 +457,20 @@ class PackBackendProtocol(PackServerProtocol): |
76 | self.resumeProducing() |
77 | return |
78 | |
79 | - send_path_as_option = False |
80 | - cmd_input = None |
81 | cmd_env = {} |
82 | write_operation = False |
83 | - if not get_capabilities_advertisement(params.get(b'version', 1)): |
84 | - if command == b'git-upload-pack': |
85 | - subcmd = b'upload-pack' |
86 | - elif command == b'git-receive-pack': |
87 | - subcmd = b'receive-pack' |
88 | - write_operation = True |
89 | - else: |
90 | - self.die(b'Unsupported command in request') |
91 | - return |
92 | - else: |
93 | - v2_command = params.get(b'command') |
94 | - if command == b'git-upload-pack' and not v2_command: |
95 | - self.expectNextCommand() |
96 | - self.transport.loseConnection() |
97 | - return |
98 | - subcmd = b'upload-pack' |
99 | - cmd_env["GIT_PROTOCOL"] = 'version=2' |
100 | - send_path_as_option = True |
101 | - # Do not include "advertise-refs" parameter. |
102 | + version = self.params.get(b'version', 0) |
103 | + cmd_env["GIT_PROTOCOL"] = 'version=%s' % version |
104 | + if version == b'2': |
105 | params.pop(b'turnip-advertise-refs', None) |
106 | - cmd_input = self.getV2CommandInput(params) |
107 | + if command == b'git-upload-pack': |
108 | + subcmd = b'upload-pack' |
109 | + elif command == b'git-receive-pack': |
110 | + subcmd = b'receive-pack' |
111 | + write_operation = True |
112 | + else: |
113 | + self.die(b'Unsupported command in request') |
114 | + return |
115 | |
116 | args = [] |
117 | if params.pop(b'turnip-stateless-rpc', None): |
118 | @@ -516,14 +480,12 @@ class PackBackendProtocol(PackServerProtocol): |
119 | args.append(self.path) |
120 | self.spawnGit( |
121 | subcmd, args, |
122 | - write_operation=write_operation, |
123 | - auth_params=auth_params, |
124 | - send_path_as_option=send_path_as_option, |
125 | - cmd_env=cmd_env, cmd_input=cmd_input) |
126 | + write_operation=write_operation, auth_params=auth_params, |
127 | + cmd_env=cmd_env) |
128 | |
129 | def spawnGit(self, subcmd, extra_args, write_operation=False, |
130 | send_path_as_option=False, auth_params=None, |
131 | - cmd_env=None, cmd_input=None): |
132 | + cmd_env=None): |
133 | cmd = b'git' |
134 | args = [b'git'] |
135 | if send_path_as_option: |
136 | @@ -545,7 +507,7 @@ class PackBackendProtocol(PackServerProtocol): |
137 | env[b'TURNIP_HOOK_RPC_KEY'] = self.hookrpc_key |
138 | |
139 | self.log.info('Spawning {args}', args=args) |
140 | - self.peer = GitProcessProtocol(self, cmd_input) |
141 | + self.peer = GitProcessProtocol(self) |
142 | self.spawnProcess(cmd, args, env=env) |
143 | |
144 | def spawnProcess(self, cmd, args, env=None): |
145 | @@ -674,7 +636,7 @@ class PackVirtServerProtocol(PackProxyServerProtocol): |
146 | @defer.inlineCallbacks |
147 | def requestReceived(self, command, pathname, params): |
148 | self.extractRequestMeta(command, pathname, params) |
149 | - permission = 'read' if command == b'git-upload-pack' else 'write' |
150 | + permission = 'read' if command != b'git-receive-pack' else 'write' |
151 | proxy = xmlrpc.Proxy(self.factory.virtinfo_endpoint, allowNone=True) |
152 | try: |
153 | auth_params = self.createAuthParams(params) |
154 | diff --git a/turnip/pack/helpers.py b/turnip/pack/helpers.py |
155 | index 93aba33..5d78d68 100644 |
156 | --- a/turnip/pack/helpers.py |
157 | +++ b/turnip/pack/helpers.py |
158 | @@ -24,10 +24,10 @@ import six |
159 | import yaml |
160 | |
161 | import turnip.pack.hooks |
162 | - |
163 | +from turnip.version_info import version_info |
164 | |
165 | FLUSH_PKT = b'0000' |
166 | -DELIM_PKT = b'0001' |
167 | +DELIM_PKT = object() |
168 | PKT_LEN_SIZE = 4 |
169 | PKT_PAYLOAD_MAX = 65520 |
170 | INCOMPLETE_PKT = object() |
171 | @@ -37,6 +37,8 @@ def encode_packet(payload): |
172 | if payload is None: |
173 | # flush-pkt. |
174 | return FLUSH_PKT |
175 | + if payload is DELIM_PKT: |
176 | + return b'0001' |
177 | else: |
178 | # data-pkt |
179 | if len(payload) > PKT_PAYLOAD_MAX: |
180 | @@ -48,63 +50,25 @@ def encode_packet(payload): |
181 | |
182 | def decode_packet(input): |
183 | """Consume a packet, returning the payload and any unconsumed tail.""" |
184 | + if input.startswith(b'0001'): |
185 | + return (DELIM_PKT, input[PKT_LEN_SIZE:]) |
186 | if len(input) < PKT_LEN_SIZE: |
187 | return (INCOMPLETE_PKT, input) |
188 | if input.startswith(FLUSH_PKT): |
189 | # flush-pkt |
190 | return (None, input[PKT_LEN_SIZE:]) |
191 | - # data-pkt |
192 | - try: |
193 | - pkt_len = int(input[:PKT_LEN_SIZE], 16) |
194 | - except ValueError: |
195 | - pkt_len = 0 |
196 | - if not (PKT_LEN_SIZE <= pkt_len <= (PKT_LEN_SIZE + PKT_PAYLOAD_MAX)): |
197 | - raise ValueError("Invalid pkt-len") |
198 | - if len(input) < pkt_len: |
199 | - # Some of the packet is yet to be received. |
200 | - return (INCOMPLETE_PKT, input) |
201 | - # v2 protocol "hides" extra parameters after the end of the packet. |
202 | - if len(input) > pkt_len and b'version=2\x00' in input: |
203 | - if FLUSH_PKT not in input: |
204 | - return INCOMPLETE_PKT, input |
205 | - end = input.index(FLUSH_PKT) |
206 | - return input[PKT_LEN_SIZE:end], input[end + len(FLUSH_PKT):] |
207 | - return (input[PKT_LEN_SIZE:pkt_len], input[pkt_len:]) |
208 | - |
209 | - |
210 | -def decode_packet_list(data): |
211 | - remaining = data |
212 | - retval = [] |
213 | - while remaining: |
214 | - pkt, remaining = decode_packet(remaining) |
215 | - retval.append(pkt) |
216 | - return retval |
217 | - |
218 | - |
219 | -def decode_protocol_v2_params(data): |
220 | - """Parse the protocol v2 extra parameters hidden behind the end of v1 |
221 | - protocol. |
222 | - |
223 | - :return: An ordered dict with parsed v2 parameters. |
224 | - """ |
225 | - params = OrderedDict() |
226 | - cmd, remaining = decode_packet(data) |
227 | - cmd = cmd.split(b'=', 1)[-1].strip() |
228 | - capabilities, args = remaining.split(DELIM_PKT) |
229 | - params[b"command"] = cmd |
230 | - params[b"capabilities"] = decode_packet_list(capabilities) |
231 | - for arg in decode_packet_list(args): |
232 | - if arg is None: |
233 | - continue |
234 | - arg = arg.strip('\n') |
235 | - if b' ' in arg: |
236 | - k, v = arg.split(b' ', 1) |
237 | - if k not in params: |
238 | - params[k] = [] |
239 | - params[k].append(v) |
240 | - else: |
241 | - params[arg] = b"" |
242 | - return params |
243 | + else: |
244 | + # data-pkt |
245 | + try: |
246 | + pkt_len = int(input[:PKT_LEN_SIZE], 16) |
247 | + except ValueError: |
248 | + pkt_len = 0 |
249 | + if not (PKT_LEN_SIZE <= pkt_len <= (PKT_LEN_SIZE + PKT_PAYLOAD_MAX)): |
250 | + raise ValueError("Invalid pkt-len") |
251 | + if len(input) < pkt_len: |
252 | + # Some of the packet is yet to be received. |
253 | + return (INCOMPLETE_PKT, input) |
254 | + return (input[PKT_LEN_SIZE:pkt_len], input[pkt_len:]) |
255 | |
256 | |
257 | def decode_request(data): |
258 | @@ -121,7 +85,7 @@ def decode_request(data): |
259 | # Following the command is a pathname, then any number of named |
260 | # parameters. Each of these is NUL-terminated. |
261 | # After that, v1 should end (v2 might have extra commands). |
262 | - if len(bits) < 2 or (b'version=2' not in bits and bits[-1] != b''): |
263 | + if len(bits) < 2 or bits[-1] != b'': |
264 | raise ValueError('Invalid git-proto-request') |
265 | pathname = bits[0] |
266 | params = OrderedDict() |
267 | @@ -139,12 +103,6 @@ def decode_request(data): |
268 | raise ValueError('Parameters must not be repeated') |
269 | params[name] = value |
270 | |
271 | - # If there are remaining bits at the end, we must be dealing with v2 |
272 | - # protocol. So, we append v2 parameters at the end of original parameters. |
273 | - if bits[-1]: |
274 | - for k, v in decode_protocol_v2_params(bits[-1]).items(): |
275 | - params[k] = v |
276 | - |
277 | return command, pathname, params |
278 | |
279 | |
280 | @@ -297,5 +255,14 @@ def get_capabilities_advertisement(version='1'): |
281 | |
282 | If no binary data is sent, no advertisement is done and we declare to |
283 | not be compatible with that specific version.""" |
284 | - # XXX pappacena 2020-08-11: Return the correct data for protocol v2. |
285 | - return b"" |
286 | + if version != '2': |
287 | + return b"" |
288 | + turnip_version = six.ensure_binary(version_info.get("revision_id", '-1')) |
289 | + return ( |
290 | + encode_packet(b"version 2\n") + |
291 | + encode_packet(b"agent=turnip/%s\n" % turnip_version) + |
292 | + encode_packet(b"ls-refs\n") + |
293 | + encode_packet(b"fetch=shallow\n") + |
294 | + encode_packet(b"server-option\n") + |
295 | + FLUSH_PKT |
296 | + ) |
297 | diff --git a/turnip/pack/http.py b/turnip/pack/http.py |
298 | index c0becf0..9e0661c 100644 |
299 | --- a/turnip/pack/http.py |
300 | +++ b/turnip/pack/http.py |
301 | @@ -29,6 +29,7 @@ from paste.auth.cookie import ( |
302 | decode as decode_cookie, |
303 | encode as encode_cookie, |
304 | ) |
305 | +import six |
306 | from twisted.internet import ( |
307 | defer, |
308 | error, |
309 | @@ -58,8 +59,8 @@ from turnip.pack.helpers import ( |
310 | encode_packet, |
311 | encode_request, |
312 | translate_xmlrpc_fault, |
313 | - TurnipFaultCode, |
314 | - ) |
315 | + TurnipFaultCode, get_capabilities_advertisement, |
316 | +) |
317 | try: |
318 | from turnip.version_info import version_info |
319 | except ImportError: |
320 | @@ -80,6 +81,16 @@ def fail_request(request, message, code=http.INTERNAL_SERVER_ERROR): |
321 | return b'' |
322 | |
323 | |
324 | +def get_protocol_version_from_request(request): |
325 | + version_header = request.requestHeaders.getRawHeaders( |
326 | + 'git-protocol', ['version=1'])[0] |
327 | + try: |
328 | + return version_header.split('version=', 1)[1] |
329 | + except IndexError: |
330 | + pass |
331 | + return 1 |
332 | + |
333 | + |
334 | class HTTPPackClientProtocol(PackProtocol): |
335 | """Abstract bridge between a Git pack connection and a smart HTTP request. |
336 | |
337 | @@ -99,7 +110,11 @@ class HTTPPackClientProtocol(PackProtocol): |
338 | |
339 | def startGoodResponse(self): |
340 | """Prepare the HTTP response for forwarding from the backend.""" |
341 | - raise NotImplementedError() |
342 | + self.factory.http_request.write( |
343 | + get_capabilities_advertisement(self.getProtocolVersion())) |
344 | + |
345 | + def getProtocolVersion(self): |
346 | + return get_protocol_version_from_request(self.factory.http_request) |
347 | |
348 | def backendConnected(self): |
349 | """Called when the backend is connected and has sent a good packet.""" |
350 | @@ -209,6 +224,7 @@ class HTTPPackClientRefsProtocol(HTTPPackClientProtocol): |
351 | self.factory.http_request.setHeader( |
352 | b'Content-Type', |
353 | b'application/x-%s-advertisement' % self.factory.command) |
354 | + super(HTTPPackClientRefsProtocol, self).startGoodResponse() |
355 | |
356 | def backendConnected(self): |
357 | HTTPPackClientProtocol.backendConnected(self) |
358 | @@ -221,10 +237,11 @@ class HTTPPackClientCommandProtocol(HTTPPackClientProtocol): |
359 | |
360 | def startGoodResponse(self): |
361 | """Prepare the HTTP response for forwarding from the backend.""" |
362 | - self.factory.http_request.setResponseCode(http.OK) |
363 | - self.factory.http_request.setHeader( |
364 | - b'Content-Type', |
365 | - b'application/x-%s-result' % self.factory.command) |
366 | + if self.getProtocolVersion() != b'2': |
367 | + self.factory.http_request.setResponseCode(http.OK) |
368 | + self.factory.http_request.setHeader( |
369 | + b'Content-Type', |
370 | + b'application/x-%s-result' % self.factory.command) |
371 | |
372 | |
373 | class HTTPPackClientFactory(protocol.ClientFactory): |
374 | @@ -280,8 +297,11 @@ class BaseSmartHTTPResource(resource.Resource): |
375 | by the virt service, if any. |
376 | """ |
377 | params = { |
378 | + b'turnip-frontend': b'http', |
379 | b'turnip-can-authenticate': b'yes', |
380 | b'turnip-request-id': str(uuid.uuid4()), |
381 | + b'version': six.ensure_binary( |
382 | + get_protocol_version_from_request(request)) |
383 | } |
384 | authenticated_params = yield self.authenticateUser(request) |
385 | for key, value in authenticated_params.items(): |
386 | diff --git a/turnip/pack/tests/test_functional.py b/turnip/pack/tests/test_functional.py |
387 | index 27bc26e..c811b2a 100644 |
388 | --- a/turnip/pack/tests/test_functional.py |
389 | +++ b/turnip/pack/tests/test_functional.py |
390 | @@ -815,16 +815,19 @@ class TestSmartHTTPFrontendWithAuthFunctional(TestSmartHTTPFrontendFunctional): |
391 | test_root = self.useFixture(TempDir()).path |
392 | clone = os.path.join(test_root, 'clone') |
393 | yield self.assertCommandSuccess((b'git', b'clone', self.ro_url, clone)) |
394 | + expected_requests = 1 if self.protocol_version == '1' else 2 |
395 | self.assertEqual( |
396 | - [(b'test-user', b'test-password')], self.virtinfo.authentications) |
397 | - self.assertThat(self.virtinfo.translations, MatchesListwise([ |
398 | - MatchesListwise([ |
399 | + [(b'test-user', b'test-password')] * expected_requests, |
400 | + self.virtinfo.authentications) |
401 | + self.assertEqual(expected_requests, len(self.virtinfo.translations)) |
402 | + for translation in self.virtinfo.translations: |
403 | + self.assertThat(translation, MatchesListwise([ |
404 | Equals(b'/test'), Equals(b'read'), |
405 | MatchesDict({ |
406 | b'can-authenticate': Is(True), |
407 | b'request-id': Not(Is(None)), |
408 | - b'user': Equals(b'test-user'), |
409 | - })])])) |
410 | + b'user': Equals(b'test-user')}) |
411 | + ])) |
412 | |
413 | @defer.inlineCallbacks |
414 | def test_authenticated_push(self): |
415 | diff --git a/turnip/pack/tests/test_git.py b/turnip/pack/tests/test_git.py |
416 | index b69049c..d1a8162 100644 |
417 | --- a/turnip/pack/tests/test_git.py |
418 | +++ b/turnip/pack/tests/test_git.py |
419 | @@ -9,7 +9,6 @@ from __future__ import ( |
420 | |
421 | import hashlib |
422 | import os.path |
423 | -from collections import OrderedDict |
424 | |
425 | from fixtures import TempDir, MonkeyPatch |
426 | from pygit2 import init_repository |
427 | @@ -35,7 +34,6 @@ from turnip.pack import ( |
428 | git, |
429 | helpers, |
430 | ) |
431 | -from turnip.pack.git import GitProcessProtocol |
432 | from turnip.pack.tests.fake_servers import FakeVirtInfoService |
433 | from turnip.pack.tests.test_hooks import MockHookRPCHandler |
434 | from turnip.tests.compat import mock |
435 | @@ -51,18 +49,6 @@ class DummyPackServerProtocol(git.PackServerProtocol): |
436 | self.test_request = (command, pathname, host) |
437 | |
438 | |
439 | -class TestGitProcessProtocol(TestCase): |
440 | - def test_can_write_to_stdin_directly(self): |
441 | - peer = mock.Mock() |
442 | - transport = mock.Mock() |
443 | - protocol = GitProcessProtocol(peer, b"this is the stdin") |
444 | - protocol.transport = transport |
445 | - protocol.connectionMade() |
446 | - self.assertEqual( |
447 | - [mock.call(b'this is the stdin', )], |
448 | - transport.write.call_args_list) |
449 | - |
450 | - |
451 | class TestPackServerProtocol(TestCase): |
452 | """Test the base implementation of the git pack network protocol.""" |
453 | |
454 | @@ -243,8 +229,9 @@ class TestPackBackendProtocol(TestCase): |
455 | [('foo.git', )], self.virtinfo.confirm_repo_creation_call_args) |
456 | |
457 | self.assertEqual( |
458 | - (b'git', [b'git', b'upload-pack', full_path], {}), |
459 | - self.proto.test_process) |
460 | + (b'git', [b'git', b'upload-pack', full_path], { |
461 | + 'GIT_PROTOCOL': 'version=0' |
462 | + }), self.proto.test_process) |
463 | |
464 | @defer.inlineCallbacks |
465 | def test_create_repo_fails_to_confirm(self): |
466 | @@ -281,47 +268,8 @@ class TestPackBackendProtocol(TestCase): |
467 | self.assertEqual( |
468 | (b'git', |
469 | [b'git', b'upload-pack', full_path], |
470 | - {}), |
471 | - self.proto.test_process) |
472 | - |
473 | - def test_git_upload_pack_v2_calls_spawnProcess(self): |
474 | - # If the command is git-upload-pack using v2 protocol, requestReceived |
475 | - # calls spawnProcess with appropriate arguments. |
476 | - advertise_capabilities = mock.Mock() |
477 | - self.useFixture( |
478 | - MonkeyPatch("turnip.pack.git.get_capabilities_advertisement", |
479 | - advertise_capabilities)) |
480 | - advertise_capabilities.return_value = b'fake capability' |
481 | - |
482 | - self.proto.requestReceived( |
483 | - b'git-upload-pack', b'/foo.git', OrderedDict([ |
484 | - (b'turnip-x', b'yes'), |
485 | - (b'turnip-request-id', b'123'), |
486 | - (b'version', b'2'), |
487 | - (b'command', b'ls-refs'), |
488 | - (b'capabilities', b'agent=git/2.25.1'), |
489 | - (b'peel', b''), |
490 | - (b'symrefs', b''), |
491 | - (b'ref-prefix', b'HEAD\nrefs/heads/\nrefs/tags/') |
492 | - ])) |
493 | - full_path = os.path.join(six.ensure_binary(self.root), b'foo.git') |
494 | - self.assertEqual( |
495 | - (b'git', |
496 | - [b'git', b'-C', full_path, b'upload-pack', full_path], |
497 | - {'GIT_PROTOCOL': 'version=2'}), |
498 | + {'GIT_PROTOCOL': 'version=0'}), |
499 | self.proto.test_process) |
500 | - stdin_content = ( |
501 | - b'0014command=ls-refs\n' |
502 | - b'0015agent=git/2.25.1\n' |
503 | - b'00010014command ls-refs\n' |
504 | - b'0009peel\n' |
505 | - b'000csymrefs\n' |
506 | - b'0014ref-prefix HEAD\n' |
507 | - b'001bref-prefix refs/heads/\n' |
508 | - b'001aref-prefix refs/tags/\n' |
509 | - b'0000' |
510 | - ) |
511 | - self.assertEqual(stdin_content, self.proto.peer.cmd_input) |
512 | |
513 | def test_git_receive_pack_calls_spawnProcess(self): |
514 | # If the command is git-receive-pack, requestReceived calls |
515 | diff --git a/turnip/pack/tests/test_helpers.py b/turnip/pack/tests/test_helpers.py |
516 | index bad7117..a5ba913 100644 |
517 | --- a/turnip/pack/tests/test_helpers.py |
518 | +++ b/turnip/pack/tests/test_helpers.py |
519 | @@ -7,25 +7,32 @@ from __future__ import ( |
520 | unicode_literals, |
521 | ) |
522 | |
523 | +from collections import OrderedDict |
524 | import os.path |
525 | import re |
526 | -from collections import OrderedDict |
527 | - |
528 | -import stat |
529 | -import sys |
530 | -from textwrap import dedent |
531 | -import time |
532 | +import shutil |
533 | +import subprocess |
534 | +import tempfile |
535 | |
536 | from fixtures import TempDir |
537 | from pygit2 import ( |
538 | Config, |
539 | init_repository, |
540 | ) |
541 | +import six |
542 | +import stat |
543 | +import sys |
544 | from testtools import TestCase |
545 | +from textwrap import dedent |
546 | +import time |
547 | |
548 | from turnip.pack import helpers |
549 | import turnip.pack.hooks |
550 | -from turnip.pack.helpers import FLUSH_PKT |
551 | +from turnip.pack.helpers import ( |
552 | + get_capabilities_advertisement, |
553 | + encode_packet, |
554 | + ) |
555 | +from turnip.version_info import version_info |
556 | |
557 | TEST_DATA = b'0123456789abcdef' |
558 | TEST_PKT = b'00140123456789abcdef' |
559 | @@ -175,27 +182,6 @@ class TestDecodeRequest(TestCase): |
560 | b'git-do-stuff /foo\0host=foo\0host=bar\0', |
561 | b'Parameters must not be repeated') |
562 | |
563 | - def test_v2_extra_commands(self): |
564 | - data = ( |
565 | - b"git-upload-pack b306d" + |
566 | - b"\x00turnip-x=yes\x00turnip-request-id=123\x00" + |
567 | - b"version=2\x000014command=ls-refs\n0014agent=git/2.25.1" + |
568 | - b'00010009peel\n000csymrefs\n0014ref-prefix HEAD\n' + |
569 | - b'001bref-prefix refs/heads/\n' |
570 | - b'001aref-prefix refs/tags/\n0000' + |
571 | - FLUSH_PKT) |
572 | - decoded = helpers.decode_request(data) |
573 | - self.assertEqual((b'git-upload-pack', b'b306d', OrderedDict([ |
574 | - (b'turnip-x', b'yes'), |
575 | - (b'turnip-request-id', b'123'), |
576 | - (b'version', b'2'), |
577 | - (b'command', b'ls-refs'), |
578 | - (b'capabilities', [b'agent=git/2.25.1']), |
579 | - (b'peel', b''), |
580 | - (b'symrefs', b''), |
581 | - (b'ref-prefix', [b'HEAD', b'refs/heads/', b'refs/tags/']) |
582 | - ])), decoded) |
583 | - |
584 | |
585 | class TestEncodeRequest(TestCase): |
586 | """Test git-proto-request encoding.""" |
587 | @@ -344,3 +330,30 @@ class TestEnsureHooks(TestCase): |
588 | self.assertEqual(expected_bytes, actual.read()) |
589 | # The hook is executable. |
590 | self.assertTrue(os.stat(self.hook('hook.py')).st_mode & stat.S_IXUSR) |
591 | + |
592 | + |
593 | +class TestCapabilityAdvertisement(TestCase): |
594 | + def test_returning_same_output_as_git_command(self): |
595 | + """Make sure that our hard-coded feature advertisement matches what |
596 | + our git command advertises.""" |
597 | + root = tempfile.mkdtemp(prefix=b'turnip-test-root-') |
598 | + self.addCleanup(shutil.rmtree, root, ignore_errors=True) |
599 | + # Create a dummy repository |
600 | + subprocess.call(['git', 'init', root]) |
601 | + |
602 | + git_version = subprocess.check_output(['git', '--version']) |
603 | + git_version_num = six.ensure_binary(git_version.split(' ')[-1].strip()) |
604 | + git_agent = encode_packet(b"agent=git/%s\n" % git_version_num) |
605 | + |
606 | + proc = subprocess.Popen( |
607 | + ['git', 'upload-pack', root], env={"GIT_PROTOCOL": "version=2"}, |
608 | + stdout=subprocess.PIPE, stdin=subprocess.PIPE) |
609 | + git_advertised_capabilities, _ = proc.communicate() |
610 | + |
611 | + turnip_capabilities = get_capabilities_advertisement(version=b'2') |
612 | + turnip_agent = encode_packet( |
613 | + b"agent=turnip/%s\n" % version_info["revision_id"]) |
614 | + |
615 | + self.assertEqual( |
616 | + turnip_capabilities, |
617 | + git_advertised_capabilities.replace(git_agent, turnip_agent)) |
618 | diff --git a/turnip/pack/tests/test_http.py b/turnip/pack/tests/test_http.py |
619 | index 26e2754..9c547c1 100644 |
620 | --- a/turnip/pack/tests/test_http.py |
621 | +++ b/turnip/pack/tests/test_http.py |
622 | @@ -24,7 +24,10 @@ from turnip.pack import ( |
623 | helpers, |
624 | http, |
625 | ) |
626 | +from turnip.pack.helpers import encode_packet |
627 | from turnip.pack.tests.fake_servers import FakeVirtInfoService |
628 | +from turnip.tests.compat import mock |
629 | +from turnip.version_info import version_info |
630 | |
631 | |
632 | class LessDummyRequest(requesthelper.DummyRequest): |
633 | @@ -74,12 +77,14 @@ class FakeRoot(object): |
634 | |
635 | def __init__(self): |
636 | self.backend_transport = None |
637 | + self.client_factory = None |
638 | self.backend_connected = defer.Deferred() |
639 | |
640 | def authenticateWithPassword(self, user, password): |
641 | return {} |
642 | |
643 | def connectToBackend(self, client_factory): |
644 | + self.client_factory = client_factory |
645 | self.backend_transport = testing.StringTransportWithDisconnection() |
646 | p = client_factory.buildProtocol(None) |
647 | self.backend_transport.protocol = p |
648 | @@ -186,6 +191,37 @@ class TestSmartHTTPRefsResource(ErrorTestMixin, TestCase): |
649 | 'And I am raw, since we got a good packet to start with.', |
650 | self.request.value) |
651 | |
652 | + @defer.inlineCallbacks |
653 | + def test_good_v2_included_version_and_capabilities(self): |
654 | + self.request.requestHeaders.addRawHeader("Git-Protocol", "version=2") |
655 | + yield self.performRequest( |
656 | + helpers.encode_packet(b'I am git protocol data.') + |
657 | + b'And I am raw, since we got a good packet to start with.') |
658 | + self.assertEqual(200, self.request.responseCode) |
659 | + self.assertEqual(self.root.client_factory.params, { |
660 | + 'version': '2', |
661 | + 'turnip-frontend': 'http', |
662 | + 'turnip-advertise-refs': 'yes', |
663 | + 'turnip-can-authenticate': 'yes', |
664 | + 'turnip-request-id': mock.ANY, |
665 | + 'turnip-stateless-rpc': 'yes'}) |
666 | + |
667 | + ver = version_info["revision_id"] |
668 | + capabilities = ( |
669 | + encode_packet(b'version 2\n') + |
670 | + encode_packet(b'agent=turnip/%s\n' % ver) + |
671 | + encode_packet(b'ls-refs\n') + |
672 | + encode_packet(b'fetch=shallow\n') + |
673 | + encode_packet(b'server-option\n') + |
674 | + b'0000' |
675 | + ) |
676 | + self.assertEqual( |
677 | + capabilities + |
678 | + '001e# service=git-upload-pack\n' |
679 | + '0000001bI am git protocol data.' |
680 | + 'And I am raw, since we got a good packet to start with.', |
681 | + self.request.value) |
682 | + |
683 | |
684 | class TestSmartHTTPCommandResource(ErrorTestMixin, TestCase): |
685 |