Merge ~pappacena/turnip:enable-http-v2 into turnip: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)
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

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
diff --git a/turnip/pack/git.py b/turnip/pack/git.py
index 1b34ac0..85fa57e 100644
--- a/turnip/pack/git.py
+++ b/turnip/pack/git.py
@@ -31,14 +31,11 @@ from turnip.config import config
31from turnip.helpers import compose_path31from turnip.helpers import compose_path
32from turnip.pack.helpers import (32from turnip.pack.helpers import (
33 decode_packet,33 decode_packet,
34 DELIM_PKT,
35 decode_request,34 decode_request,
36 encode_packet,35 encode_packet,
37 encode_request,36 encode_request,
38 ensure_config,37 ensure_config,
39 ensure_hooks,38 ensure_hooks,
40 FLUSH_PKT,
41 get_capabilities_advertisement,
42 INCOMPLETE_PKT,39 INCOMPLETE_PKT,
43 translate_xmlrpc_fault,40 translate_xmlrpc_fault,
44 )41 )
@@ -242,20 +239,15 @@ class GitProcessProtocol(protocol.ProcessProtocol):
242239
243 _err_buffer = b''240 _err_buffer = b''
244241
245 def __init__(self, peer, cmd_input=None):242 def __init__(self, peer):
246 self.peer = peer243 self.peer = peer
247 self.cmd_input = cmd_input
248 self.out_started = False244 self.out_started = False
249245
250 def connectionMade(self):246 def connectionMade(self):
251 self.peer.setPeer(self)247 self.peer.setPeer(self)
252 self.peer.transport.registerProducer(self, True)248 self.peer.transport.registerProducer(self, True)
253 if not self.cmd_input:249 self.transport.registerProducer(
254 self.transport.registerProducer(250 UnstoppableProducerWrapper(self.peer.transport), True)
255 UnstoppableProducerWrapper(self.peer.transport), True)
256 else:
257 self.transport.write(self.cmd_input)
258 self.loseWriteConnection()
259 self.peer.resumeProducing()251 self.peer.resumeProducing()
260252
261 def outReceived(self, data):253 def outReceived(self, data):
@@ -440,29 +432,12 @@ class PackBackendProtocol(PackServerProtocol):
440 hookrpc_key = None432 hookrpc_key = None
441 expect_set_symbolic_ref = False433 expect_set_symbolic_ref = False
442434
443 def getV2CommandInput(self, params):
444 """Reconstruct what should be sent to git's stdin from the
445 parameters received."""
446 cmd_input = encode_packet(b"command=%s\n" % params.get(b'command'))
447 for capability in params["capabilities"].split(b"\n"):
448 cmd_input += encode_packet(b"%s\n" % capability)
449 cmd_input += DELIM_PKT
450 ignore_keys = (b'capabilities', b'version')
451 for k, v in params.items():
452 k = six.ensure_binary(k)
453 if k.startswith(b"turnip-") or k in ignore_keys:
454 continue
455 for param_value in v.split(b'\n'):
456 value = (b"" if not param_value else b" %s" % param_value)
457 cmd_input += encode_packet(b"%s%s\n" % (k, value))
458 cmd_input += FLUSH_PKT
459 return cmd_input
460
461 @defer.inlineCallbacks435 @defer.inlineCallbacks
462 def requestReceived(self, command, raw_pathname, params):436 def requestReceived(self, command, raw_pathname, params):
463 self.extractRequestMeta(command, raw_pathname, params)437 self.extractRequestMeta(command, raw_pathname, params)
464 self.command = command438 self.command = command
465 self.raw_pathname = raw_pathname439 self.raw_pathname = raw_pathname
440 self.params = params
466 self.path = compose_path(self.factory.root, self.raw_pathname)441 self.path = compose_path(self.factory.root, self.raw_pathname)
467 auth_params = self.createAuthParams(params)442 auth_params = self.createAuthParams(params)
468443
@@ -482,31 +457,20 @@ class PackBackendProtocol(PackServerProtocol):
482 self.resumeProducing()457 self.resumeProducing()
483 return458 return
484459
485 send_path_as_option = False
486 cmd_input = None
487 cmd_env = {}460 cmd_env = {}
488 write_operation = False461 write_operation = False
489 if not get_capabilities_advertisement(params.get(b'version', 1)):462 version = self.params.get(b'version', 0)
490 if command == b'git-upload-pack':463 cmd_env["GIT_PROTOCOL"] = 'version=%s' % version
491 subcmd = b'upload-pack'464 if version == b'2':
492 elif command == b'git-receive-pack':
493 subcmd = b'receive-pack'
494 write_operation = True
495 else:
496 self.die(b'Unsupported command in request')
497 return
498 else:
499 v2_command = params.get(b'command')
500 if command == b'git-upload-pack' and not v2_command:
501 self.expectNextCommand()
502 self.transport.loseConnection()
503 return
504 subcmd = b'upload-pack'
505 cmd_env["GIT_PROTOCOL"] = 'version=2'
506 send_path_as_option = True
507 # Do not include "advertise-refs" parameter.
508 params.pop(b'turnip-advertise-refs', None)465 params.pop(b'turnip-advertise-refs', None)
509 cmd_input = self.getV2CommandInput(params)466 if command == b'git-upload-pack':
467 subcmd = b'upload-pack'
468 elif command == b'git-receive-pack':
469 subcmd = b'receive-pack'
470 write_operation = True
471 else:
472 self.die(b'Unsupported command in request')
473 return
510474
511 args = []475 args = []
512 if params.pop(b'turnip-stateless-rpc', None):476 if params.pop(b'turnip-stateless-rpc', None):
@@ -516,14 +480,12 @@ class PackBackendProtocol(PackServerProtocol):
516 args.append(self.path)480 args.append(self.path)
517 self.spawnGit(481 self.spawnGit(
518 subcmd, args,482 subcmd, args,
519 write_operation=write_operation,483 write_operation=write_operation, auth_params=auth_params,
520 auth_params=auth_params,484 cmd_env=cmd_env)
521 send_path_as_option=send_path_as_option,
522 cmd_env=cmd_env, cmd_input=cmd_input)
523485
524 def spawnGit(self, subcmd, extra_args, write_operation=False,486 def spawnGit(self, subcmd, extra_args, write_operation=False,
525 send_path_as_option=False, auth_params=None,487 send_path_as_option=False, auth_params=None,
526 cmd_env=None, cmd_input=None):488 cmd_env=None):
527 cmd = b'git'489 cmd = b'git'
528 args = [b'git']490 args = [b'git']
529 if send_path_as_option:491 if send_path_as_option:
@@ -545,7 +507,7 @@ class PackBackendProtocol(PackServerProtocol):
545 env[b'TURNIP_HOOK_RPC_KEY'] = self.hookrpc_key507 env[b'TURNIP_HOOK_RPC_KEY'] = self.hookrpc_key
546508
547 self.log.info('Spawning {args}', args=args)509 self.log.info('Spawning {args}', args=args)
548 self.peer = GitProcessProtocol(self, cmd_input)510 self.peer = GitProcessProtocol(self)
549 self.spawnProcess(cmd, args, env=env)511 self.spawnProcess(cmd, args, env=env)
550512
551 def spawnProcess(self, cmd, args, env=None):513 def spawnProcess(self, cmd, args, env=None):
@@ -674,7 +636,7 @@ class PackVirtServerProtocol(PackProxyServerProtocol):
674 @defer.inlineCallbacks636 @defer.inlineCallbacks
675 def requestReceived(self, command, pathname, params):637 def requestReceived(self, command, pathname, params):
676 self.extractRequestMeta(command, pathname, params)638 self.extractRequestMeta(command, pathname, params)
677 permission = 'read' if command == b'git-upload-pack' else 'write'639 permission = 'read' if command != b'git-receive-pack' else 'write'
678 proxy = xmlrpc.Proxy(self.factory.virtinfo_endpoint, allowNone=True)640 proxy = xmlrpc.Proxy(self.factory.virtinfo_endpoint, allowNone=True)
679 try:641 try:
680 auth_params = self.createAuthParams(params)642 auth_params = self.createAuthParams(params)
diff --git a/turnip/pack/helpers.py b/turnip/pack/helpers.py
index 93aba33..5d78d68 100644
--- a/turnip/pack/helpers.py
+++ b/turnip/pack/helpers.py
@@ -24,10 +24,10 @@ import six
24import yaml24import yaml
2525
26import turnip.pack.hooks26import turnip.pack.hooks
2727from turnip.version_info import version_info
2828
29FLUSH_PKT = b'0000'29FLUSH_PKT = b'0000'
30DELIM_PKT = b'0001'30DELIM_PKT = object()
31PKT_LEN_SIZE = 431PKT_LEN_SIZE = 4
32PKT_PAYLOAD_MAX = 6552032PKT_PAYLOAD_MAX = 65520
33INCOMPLETE_PKT = object()33INCOMPLETE_PKT = object()
@@ -37,6 +37,8 @@ def encode_packet(payload):
37 if payload is None:37 if payload is None:
38 # flush-pkt.38 # flush-pkt.
39 return FLUSH_PKT39 return FLUSH_PKT
40 if payload is DELIM_PKT:
41 return b'0001'
40 else:42 else:
41 # data-pkt43 # data-pkt
42 if len(payload) > PKT_PAYLOAD_MAX:44 if len(payload) > PKT_PAYLOAD_MAX:
@@ -48,63 +50,25 @@ def encode_packet(payload):
4850
49def decode_packet(input):51def decode_packet(input):
50 """Consume a packet, returning the payload and any unconsumed tail."""52 """Consume a packet, returning the payload and any unconsumed tail."""
53 if input.startswith(b'0001'):
54 return (DELIM_PKT, input[PKT_LEN_SIZE:])
51 if len(input) < PKT_LEN_SIZE:55 if len(input) < PKT_LEN_SIZE:
52 return (INCOMPLETE_PKT, input)56 return (INCOMPLETE_PKT, input)
53 if input.startswith(FLUSH_PKT):57 if input.startswith(FLUSH_PKT):
54 # flush-pkt58 # flush-pkt
55 return (None, input[PKT_LEN_SIZE:])59 return (None, input[PKT_LEN_SIZE:])
56 # data-pkt60 else:
57 try:61 # data-pkt
58 pkt_len = int(input[:PKT_LEN_SIZE], 16)62 try:
59 except ValueError:63 pkt_len = int(input[:PKT_LEN_SIZE], 16)
60 pkt_len = 064 except ValueError:
61 if not (PKT_LEN_SIZE <= pkt_len <= (PKT_LEN_SIZE + PKT_PAYLOAD_MAX)):65 pkt_len = 0
62 raise ValueError("Invalid pkt-len")66 if not (PKT_LEN_SIZE <= pkt_len <= (PKT_LEN_SIZE + PKT_PAYLOAD_MAX)):
63 if len(input) < pkt_len:67 raise ValueError("Invalid pkt-len")
64 # Some of the packet is yet to be received.68 if len(input) < pkt_len:
65 return (INCOMPLETE_PKT, input)69 # Some of the packet is yet to be received.
66 # v2 protocol "hides" extra parameters after the end of the packet.70 return (INCOMPLETE_PKT, input)
67 if len(input) > pkt_len and b'version=2\x00' in input:71 return (input[PKT_LEN_SIZE:pkt_len], input[pkt_len:])
68 if FLUSH_PKT not in input:
69 return INCOMPLETE_PKT, input
70 end = input.index(FLUSH_PKT)
71 return input[PKT_LEN_SIZE:end], input[end + len(FLUSH_PKT):]
72 return (input[PKT_LEN_SIZE:pkt_len], input[pkt_len:])
73
74
75def decode_packet_list(data):
76 remaining = data
77 retval = []
78 while remaining:
79 pkt, remaining = decode_packet(remaining)
80 retval.append(pkt)
81 return retval
82
83
84def decode_protocol_v2_params(data):
85 """Parse the protocol v2 extra parameters hidden behind the end of v1
86 protocol.
87
88 :return: An ordered dict with parsed v2 parameters.
89 """
90 params = OrderedDict()
91 cmd, remaining = decode_packet(data)
92 cmd = cmd.split(b'=', 1)[-1].strip()
93 capabilities, args = remaining.split(DELIM_PKT)
94 params[b"command"] = cmd
95 params[b"capabilities"] = decode_packet_list(capabilities)
96 for arg in decode_packet_list(args):
97 if arg is None:
98 continue
99 arg = arg.strip('\n')
100 if b' ' in arg:
101 k, v = arg.split(b' ', 1)
102 if k not in params:
103 params[k] = []
104 params[k].append(v)
105 else:
106 params[arg] = b""
107 return params
10872
10973
110def decode_request(data):74def decode_request(data):
@@ -121,7 +85,7 @@ def decode_request(data):
121 # Following the command is a pathname, then any number of named85 # Following the command is a pathname, then any number of named
122 # parameters. Each of these is NUL-terminated.86 # parameters. Each of these is NUL-terminated.
123 # After that, v1 should end (v2 might have extra commands).87 # After that, v1 should end (v2 might have extra commands).
124 if len(bits) < 2 or (b'version=2' not in bits and bits[-1] != b''):88 if len(bits) < 2 or bits[-1] != b'':
125 raise ValueError('Invalid git-proto-request')89 raise ValueError('Invalid git-proto-request')
126 pathname = bits[0]90 pathname = bits[0]
127 params = OrderedDict()91 params = OrderedDict()
@@ -139,12 +103,6 @@ def decode_request(data):
139 raise ValueError('Parameters must not be repeated')103 raise ValueError('Parameters must not be repeated')
140 params[name] = value104 params[name] = value
141105
142 # If there are remaining bits at the end, we must be dealing with v2
143 # protocol. So, we append v2 parameters at the end of original parameters.
144 if bits[-1]:
145 for k, v in decode_protocol_v2_params(bits[-1]).items():
146 params[k] = v
147
148 return command, pathname, params106 return command, pathname, params
149107
150108
@@ -297,5 +255,14 @@ def get_capabilities_advertisement(version='1'):
297255
298 If no binary data is sent, no advertisement is done and we declare to256 If no binary data is sent, no advertisement is done and we declare to
299 not be compatible with that specific version."""257 not be compatible with that specific version."""
300 # XXX pappacena 2020-08-11: Return the correct data for protocol v2.258 if version != '2':
301 return b""259 return b""
260 turnip_version = six.ensure_binary(version_info.get("revision_id", '-1'))
261 return (
262 encode_packet(b"version 2\n") +
263 encode_packet(b"agent=turnip/%s\n" % turnip_version) +
264 encode_packet(b"ls-refs\n") +
265 encode_packet(b"fetch=shallow\n") +
266 encode_packet(b"server-option\n") +
267 FLUSH_PKT
268 )
diff --git a/turnip/pack/http.py b/turnip/pack/http.py
index c0becf0..9e0661c 100644
--- a/turnip/pack/http.py
+++ b/turnip/pack/http.py
@@ -29,6 +29,7 @@ from paste.auth.cookie import (
29 decode as decode_cookie,29 decode as decode_cookie,
30 encode as encode_cookie,30 encode as encode_cookie,
31 )31 )
32import six
32from twisted.internet import (33from twisted.internet import (
33 defer,34 defer,
34 error,35 error,
@@ -58,8 +59,8 @@ from turnip.pack.helpers import (
58 encode_packet,59 encode_packet,
59 encode_request,60 encode_request,
60 translate_xmlrpc_fault,61 translate_xmlrpc_fault,
61 TurnipFaultCode,62 TurnipFaultCode, get_capabilities_advertisement,
62 )63)
63try:64try:
64 from turnip.version_info import version_info65 from turnip.version_info import version_info
65except ImportError:66except ImportError:
@@ -80,6 +81,16 @@ def fail_request(request, message, code=http.INTERNAL_SERVER_ERROR):
80 return b''81 return b''
8182
8283
84def get_protocol_version_from_request(request):
85 version_header = request.requestHeaders.getRawHeaders(
86 'git-protocol', ['version=1'])[0]
87 try:
88 return version_header.split('version=', 1)[1]
89 except IndexError:
90 pass
91 return 1
92
93
83class HTTPPackClientProtocol(PackProtocol):94class HTTPPackClientProtocol(PackProtocol):
84 """Abstract bridge between a Git pack connection and a smart HTTP request.95 """Abstract bridge between a Git pack connection and a smart HTTP request.
8596
@@ -99,7 +110,11 @@ class HTTPPackClientProtocol(PackProtocol):
99110
100 def startGoodResponse(self):111 def startGoodResponse(self):
101 """Prepare the HTTP response for forwarding from the backend."""112 """Prepare the HTTP response for forwarding from the backend."""
102 raise NotImplementedError()113 self.factory.http_request.write(
114 get_capabilities_advertisement(self.getProtocolVersion()))
115
116 def getProtocolVersion(self):
117 return get_protocol_version_from_request(self.factory.http_request)
103118
104 def backendConnected(self):119 def backendConnected(self):
105 """Called when the backend is connected and has sent a good packet."""120 """Called when the backend is connected and has sent a good packet."""
@@ -209,6 +224,7 @@ class HTTPPackClientRefsProtocol(HTTPPackClientProtocol):
209 self.factory.http_request.setHeader(224 self.factory.http_request.setHeader(
210 b'Content-Type',225 b'Content-Type',
211 b'application/x-%s-advertisement' % self.factory.command)226 b'application/x-%s-advertisement' % self.factory.command)
227 super(HTTPPackClientRefsProtocol, self).startGoodResponse()
212228
213 def backendConnected(self):229 def backendConnected(self):
214 HTTPPackClientProtocol.backendConnected(self)230 HTTPPackClientProtocol.backendConnected(self)
@@ -221,10 +237,11 @@ class HTTPPackClientCommandProtocol(HTTPPackClientProtocol):
221237
222 def startGoodResponse(self):238 def startGoodResponse(self):
223 """Prepare the HTTP response for forwarding from the backend."""239 """Prepare the HTTP response for forwarding from the backend."""
224 self.factory.http_request.setResponseCode(http.OK)240 if self.getProtocolVersion() != b'2':
225 self.factory.http_request.setHeader(241 self.factory.http_request.setResponseCode(http.OK)
226 b'Content-Type',242 self.factory.http_request.setHeader(
227 b'application/x-%s-result' % self.factory.command)243 b'Content-Type',
244 b'application/x-%s-result' % self.factory.command)
228245
229246
230class HTTPPackClientFactory(protocol.ClientFactory):247class HTTPPackClientFactory(protocol.ClientFactory):
@@ -280,8 +297,11 @@ class BaseSmartHTTPResource(resource.Resource):
280 by the virt service, if any.297 by the virt service, if any.
281 """298 """
282 params = {299 params = {
300 b'turnip-frontend': b'http',
283 b'turnip-can-authenticate': b'yes',301 b'turnip-can-authenticate': b'yes',
284 b'turnip-request-id': str(uuid.uuid4()),302 b'turnip-request-id': str(uuid.uuid4()),
303 b'version': six.ensure_binary(
304 get_protocol_version_from_request(request))
285 }305 }
286 authenticated_params = yield self.authenticateUser(request)306 authenticated_params = yield self.authenticateUser(request)
287 for key, value in authenticated_params.items():307 for key, value in authenticated_params.items():
diff --git a/turnip/pack/tests/test_functional.py b/turnip/pack/tests/test_functional.py
index 27bc26e..c811b2a 100644
--- a/turnip/pack/tests/test_functional.py
+++ b/turnip/pack/tests/test_functional.py
@@ -815,16 +815,19 @@ class TestSmartHTTPFrontendWithAuthFunctional(TestSmartHTTPFrontendFunctional):
815 test_root = self.useFixture(TempDir()).path815 test_root = self.useFixture(TempDir()).path
816 clone = os.path.join(test_root, 'clone')816 clone = os.path.join(test_root, 'clone')
817 yield self.assertCommandSuccess((b'git', b'clone', self.ro_url, clone))817 yield self.assertCommandSuccess((b'git', b'clone', self.ro_url, clone))
818 expected_requests = 1 if self.protocol_version == '1' else 2
818 self.assertEqual(819 self.assertEqual(
819 [(b'test-user', b'test-password')], self.virtinfo.authentications)820 [(b'test-user', b'test-password')] * expected_requests,
820 self.assertThat(self.virtinfo.translations, MatchesListwise([821 self.virtinfo.authentications)
821 MatchesListwise([822 self.assertEqual(expected_requests, len(self.virtinfo.translations))
823 for translation in self.virtinfo.translations:
824 self.assertThat(translation, MatchesListwise([
822 Equals(b'/test'), Equals(b'read'),825 Equals(b'/test'), Equals(b'read'),
823 MatchesDict({826 MatchesDict({
824 b'can-authenticate': Is(True),827 b'can-authenticate': Is(True),
825 b'request-id': Not(Is(None)),828 b'request-id': Not(Is(None)),
826 b'user': Equals(b'test-user'),829 b'user': Equals(b'test-user')})
827 })])]))830 ]))
828831
829 @defer.inlineCallbacks832 @defer.inlineCallbacks
830 def test_authenticated_push(self):833 def test_authenticated_push(self):
diff --git a/turnip/pack/tests/test_git.py b/turnip/pack/tests/test_git.py
index b69049c..d1a8162 100644
--- a/turnip/pack/tests/test_git.py
+++ b/turnip/pack/tests/test_git.py
@@ -9,7 +9,6 @@ from __future__ import (
99
10import hashlib10import hashlib
11import os.path11import os.path
12from collections import OrderedDict
1312
14from fixtures import TempDir, MonkeyPatch13from fixtures import TempDir, MonkeyPatch
15from pygit2 import init_repository14from pygit2 import init_repository
@@ -35,7 +34,6 @@ from turnip.pack import (
35 git,34 git,
36 helpers,35 helpers,
37 )36 )
38from turnip.pack.git import GitProcessProtocol
39from turnip.pack.tests.fake_servers import FakeVirtInfoService37from turnip.pack.tests.fake_servers import FakeVirtInfoService
40from turnip.pack.tests.test_hooks import MockHookRPCHandler38from turnip.pack.tests.test_hooks import MockHookRPCHandler
41from turnip.tests.compat import mock39from turnip.tests.compat import mock
@@ -51,18 +49,6 @@ class DummyPackServerProtocol(git.PackServerProtocol):
51 self.test_request = (command, pathname, host)49 self.test_request = (command, pathname, host)
5250
5351
54class TestGitProcessProtocol(TestCase):
55 def test_can_write_to_stdin_directly(self):
56 peer = mock.Mock()
57 transport = mock.Mock()
58 protocol = GitProcessProtocol(peer, b"this is the stdin")
59 protocol.transport = transport
60 protocol.connectionMade()
61 self.assertEqual(
62 [mock.call(b'this is the stdin', )],
63 transport.write.call_args_list)
64
65
66class TestPackServerProtocol(TestCase):52class TestPackServerProtocol(TestCase):
67 """Test the base implementation of the git pack network protocol."""53 """Test the base implementation of the git pack network protocol."""
6854
@@ -243,8 +229,9 @@ class TestPackBackendProtocol(TestCase):
243 [('foo.git', )], self.virtinfo.confirm_repo_creation_call_args)229 [('foo.git', )], self.virtinfo.confirm_repo_creation_call_args)
244230
245 self.assertEqual(231 self.assertEqual(
246 (b'git', [b'git', b'upload-pack', full_path], {}),232 (b'git', [b'git', b'upload-pack', full_path], {
247 self.proto.test_process)233 'GIT_PROTOCOL': 'version=0'
234 }), self.proto.test_process)
248235
249 @defer.inlineCallbacks236 @defer.inlineCallbacks
250 def test_create_repo_fails_to_confirm(self):237 def test_create_repo_fails_to_confirm(self):
@@ -281,47 +268,8 @@ class TestPackBackendProtocol(TestCase):
281 self.assertEqual(268 self.assertEqual(
282 (b'git',269 (b'git',
283 [b'git', b'upload-pack', full_path],270 [b'git', b'upload-pack', full_path],
284 {}),271 {'GIT_PROTOCOL': 'version=0'}),
285 self.proto.test_process)
286
287 def test_git_upload_pack_v2_calls_spawnProcess(self):
288 # If the command is git-upload-pack using v2 protocol, requestReceived
289 # calls spawnProcess with appropriate arguments.
290 advertise_capabilities = mock.Mock()
291 self.useFixture(
292 MonkeyPatch("turnip.pack.git.get_capabilities_advertisement",
293 advertise_capabilities))
294 advertise_capabilities.return_value = b'fake capability'
295
296 self.proto.requestReceived(
297 b'git-upload-pack', b'/foo.git', OrderedDict([
298 (b'turnip-x', b'yes'),
299 (b'turnip-request-id', b'123'),
300 (b'version', b'2'),
301 (b'command', b'ls-refs'),
302 (b'capabilities', b'agent=git/2.25.1'),
303 (b'peel', b''),
304 (b'symrefs', b''),
305 (b'ref-prefix', b'HEAD\nrefs/heads/\nrefs/tags/')
306 ]))
307 full_path = os.path.join(six.ensure_binary(self.root), b'foo.git')
308 self.assertEqual(
309 (b'git',
310 [b'git', b'-C', full_path, b'upload-pack', full_path],
311 {'GIT_PROTOCOL': 'version=2'}),
312 self.proto.test_process)272 self.proto.test_process)
313 stdin_content = (
314 b'0014command=ls-refs\n'
315 b'0015agent=git/2.25.1\n'
316 b'00010014command ls-refs\n'
317 b'0009peel\n'
318 b'000csymrefs\n'
319 b'0014ref-prefix HEAD\n'
320 b'001bref-prefix refs/heads/\n'
321 b'001aref-prefix refs/tags/\n'
322 b'0000'
323 )
324 self.assertEqual(stdin_content, self.proto.peer.cmd_input)
325273
326 def test_git_receive_pack_calls_spawnProcess(self):274 def test_git_receive_pack_calls_spawnProcess(self):
327 # If the command is git-receive-pack, requestReceived calls275 # If the command is git-receive-pack, requestReceived calls
diff --git a/turnip/pack/tests/test_helpers.py b/turnip/pack/tests/test_helpers.py
index bad7117..a5ba913 100644
--- a/turnip/pack/tests/test_helpers.py
+++ b/turnip/pack/tests/test_helpers.py
@@ -7,25 +7,32 @@ from __future__ import (
7 unicode_literals,7 unicode_literals,
8 )8 )
99
10from collections import OrderedDict
10import os.path11import os.path
11import re12import re
12from collections import OrderedDict13import shutil
1314import subprocess
14import stat15import tempfile
15import sys
16from textwrap import dedent
17import time
1816
19from fixtures import TempDir17from fixtures import TempDir
20from pygit2 import (18from pygit2 import (
21 Config,19 Config,
22 init_repository,20 init_repository,
23 )21 )
22import six
23import stat
24import sys
24from testtools import TestCase25from testtools import TestCase
26from textwrap import dedent
27import time
2528
26from turnip.pack import helpers29from turnip.pack import helpers
27import turnip.pack.hooks30import turnip.pack.hooks
28from turnip.pack.helpers import FLUSH_PKT31from turnip.pack.helpers import (
32 get_capabilities_advertisement,
33 encode_packet,
34 )
35from turnip.version_info import version_info
2936
30TEST_DATA = b'0123456789abcdef'37TEST_DATA = b'0123456789abcdef'
31TEST_PKT = b'00140123456789abcdef'38TEST_PKT = b'00140123456789abcdef'
@@ -175,27 +182,6 @@ class TestDecodeRequest(TestCase):
175 b'git-do-stuff /foo\0host=foo\0host=bar\0',182 b'git-do-stuff /foo\0host=foo\0host=bar\0',
176 b'Parameters must not be repeated')183 b'Parameters must not be repeated')
177184
178 def test_v2_extra_commands(self):
179 data = (
180 b"git-upload-pack b306d" +
181 b"\x00turnip-x=yes\x00turnip-request-id=123\x00" +
182 b"version=2\x000014command=ls-refs\n0014agent=git/2.25.1" +
183 b'00010009peel\n000csymrefs\n0014ref-prefix HEAD\n' +
184 b'001bref-prefix refs/heads/\n'
185 b'001aref-prefix refs/tags/\n0000' +
186 FLUSH_PKT)
187 decoded = helpers.decode_request(data)
188 self.assertEqual((b'git-upload-pack', b'b306d', OrderedDict([
189 (b'turnip-x', b'yes'),
190 (b'turnip-request-id', b'123'),
191 (b'version', b'2'),
192 (b'command', b'ls-refs'),
193 (b'capabilities', [b'agent=git/2.25.1']),
194 (b'peel', b''),
195 (b'symrefs', b''),
196 (b'ref-prefix', [b'HEAD', b'refs/heads/', b'refs/tags/'])
197 ])), decoded)
198
199185
200class TestEncodeRequest(TestCase):186class TestEncodeRequest(TestCase):
201 """Test git-proto-request encoding."""187 """Test git-proto-request encoding."""
@@ -344,3 +330,30 @@ class TestEnsureHooks(TestCase):
344 self.assertEqual(expected_bytes, actual.read())330 self.assertEqual(expected_bytes, actual.read())
345 # The hook is executable.331 # The hook is executable.
346 self.assertTrue(os.stat(self.hook('hook.py')).st_mode & stat.S_IXUSR)332 self.assertTrue(os.stat(self.hook('hook.py')).st_mode & stat.S_IXUSR)
333
334
335class TestCapabilityAdvertisement(TestCase):
336 def test_returning_same_output_as_git_command(self):
337 """Make sure that our hard-coded feature advertisement matches what
338 our git command advertises."""
339 root = tempfile.mkdtemp(prefix=b'turnip-test-root-')
340 self.addCleanup(shutil.rmtree, root, ignore_errors=True)
341 # Create a dummy repository
342 subprocess.call(['git', 'init', root])
343
344 git_version = subprocess.check_output(['git', '--version'])
345 git_version_num = six.ensure_binary(git_version.split(' ')[-1].strip())
346 git_agent = encode_packet(b"agent=git/%s\n" % git_version_num)
347
348 proc = subprocess.Popen(
349 ['git', 'upload-pack', root], env={"GIT_PROTOCOL": "version=2"},
350 stdout=subprocess.PIPE, stdin=subprocess.PIPE)
351 git_advertised_capabilities, _ = proc.communicate()
352
353 turnip_capabilities = get_capabilities_advertisement(version=b'2')
354 turnip_agent = encode_packet(
355 b"agent=turnip/%s\n" % version_info["revision_id"])
356
357 self.assertEqual(
358 turnip_capabilities,
359 git_advertised_capabilities.replace(git_agent, turnip_agent))
diff --git a/turnip/pack/tests/test_http.py b/turnip/pack/tests/test_http.py
index 26e2754..9c547c1 100644
--- a/turnip/pack/tests/test_http.py
+++ b/turnip/pack/tests/test_http.py
@@ -24,7 +24,10 @@ from turnip.pack import (
24 helpers,24 helpers,
25 http,25 http,
26 )26 )
27from turnip.pack.helpers import encode_packet
27from turnip.pack.tests.fake_servers import FakeVirtInfoService28from turnip.pack.tests.fake_servers import FakeVirtInfoService
29from turnip.tests.compat import mock
30from turnip.version_info import version_info
2831
2932
30class LessDummyRequest(requesthelper.DummyRequest):33class LessDummyRequest(requesthelper.DummyRequest):
@@ -74,12 +77,14 @@ class FakeRoot(object):
7477
75 def __init__(self):78 def __init__(self):
76 self.backend_transport = None79 self.backend_transport = None
80 self.client_factory = None
77 self.backend_connected = defer.Deferred()81 self.backend_connected = defer.Deferred()
7882
79 def authenticateWithPassword(self, user, password):83 def authenticateWithPassword(self, user, password):
80 return {}84 return {}
8185
82 def connectToBackend(self, client_factory):86 def connectToBackend(self, client_factory):
87 self.client_factory = client_factory
83 self.backend_transport = testing.StringTransportWithDisconnection()88 self.backend_transport = testing.StringTransportWithDisconnection()
84 p = client_factory.buildProtocol(None)89 p = client_factory.buildProtocol(None)
85 self.backend_transport.protocol = p90 self.backend_transport.protocol = p
@@ -186,6 +191,37 @@ class TestSmartHTTPRefsResource(ErrorTestMixin, TestCase):
186 'And I am raw, since we got a good packet to start with.',191 'And I am raw, since we got a good packet to start with.',
187 self.request.value)192 self.request.value)
188193
194 @defer.inlineCallbacks
195 def test_good_v2_included_version_and_capabilities(self):
196 self.request.requestHeaders.addRawHeader("Git-Protocol", "version=2")
197 yield self.performRequest(
198 helpers.encode_packet(b'I am git protocol data.') +
199 b'And I am raw, since we got a good packet to start with.')
200 self.assertEqual(200, self.request.responseCode)
201 self.assertEqual(self.root.client_factory.params, {
202 'version': '2',
203 'turnip-frontend': 'http',
204 'turnip-advertise-refs': 'yes',
205 'turnip-can-authenticate': 'yes',
206 'turnip-request-id': mock.ANY,
207 'turnip-stateless-rpc': 'yes'})
208
209 ver = version_info["revision_id"]
210 capabilities = (
211 encode_packet(b'version 2\n') +
212 encode_packet(b'agent=turnip/%s\n' % ver) +
213 encode_packet(b'ls-refs\n') +
214 encode_packet(b'fetch=shallow\n') +
215 encode_packet(b'server-option\n') +
216 b'0000'
217 )
218 self.assertEqual(
219 capabilities +
220 '001e# service=git-upload-pack\n'
221 '0000001bI am git protocol data.'
222 'And I am raw, since we got a good packet to start with.',
223 self.request.value)
224
189225
190class TestSmartHTTPCommandResource(ErrorTestMixin, TestCase):226class TestSmartHTTPCommandResource(ErrorTestMixin, TestCase):
191227

Subscribers

People subscribed via source and target branches