##// END OF EJS Templates
svn: Avoid chunked transfer for Subversion
johbo -
r282:5d91eb8d stable
parent child Browse files
Show More
@@ -1,130 +1,130 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2010-2016 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 from urlparse import urljoin
22 22
23 23 import requests
24 24
25 25 import rhodecode
26 26 from rhodecode.lib.middleware import simplevcs
27 27 from rhodecode.lib.utils import is_valid_repo
28 28
29 29
30 30 class SimpleSvnApp(object):
31 31 IGNORED_HEADERS = [
32 32 'connection', 'keep-alive', 'content-encoding',
33 33 'transfer-encoding']
34 34
35 35 def __init__(self, config):
36 36 self.config = config
37 37
38 38 def __call__(self, environ, start_response):
39 39 request_headers = self._get_request_headers(environ)
40 40
41 41 data = environ['wsgi.input']
42 # johbo: On Gunicorn, we end up with a 415 response if we pass data
43 # to requests. I think the request is usually without payload, still
44 # reading the data to be on the safe side.
45 if environ['REQUEST_METHOD'] == 'MKCOL':
42 # johbo: Avoid that we end up with sending the request in chunked
43 # transfer encoding (mainly on Gunicorn). If we know the content
44 # length, then we should transfer the payload in one request.
45 if environ['REQUEST_METHOD'] == 'MKCOL' or 'CONTENT_LENGTH' in environ:
46 46 data = data.read()
47 47
48 48 response = requests.request(
49 49 environ['REQUEST_METHOD'], self._get_url(environ['PATH_INFO']),
50 50 data=data, headers=request_headers)
51 51
52 52 response_headers = self._get_response_headers(response.headers)
53 53 start_response(
54 54 '{} {}'.format(response.status_code, response.reason),
55 55 response_headers)
56 56 return response.iter_content(chunk_size=1024)
57 57
58 58 def _get_url(self, path):
59 59 return urljoin(
60 60 self.config.get('subversion_http_server_url', ''), path)
61 61
62 62 def _get_request_headers(self, environ):
63 63 headers = {}
64 64
65 65 for key in environ:
66 66 if not key.startswith('HTTP_'):
67 67 continue
68 68 new_key = key.split('_')
69 69 new_key = [k.capitalize() for k in new_key[1:]]
70 70 new_key = '-'.join(new_key)
71 71 headers[new_key] = environ[key]
72 72
73 73 if 'CONTENT_TYPE' in environ:
74 74 headers['Content-Type'] = environ['CONTENT_TYPE']
75 75
76 76 if 'CONTENT_LENGTH' in environ:
77 77 headers['Content-Length'] = environ['CONTENT_LENGTH']
78 78
79 79 return headers
80 80
81 81 def _get_response_headers(self, headers):
82 82 return [
83 83 (h, headers[h])
84 84 for h in headers
85 85 if h.lower() not in self.IGNORED_HEADERS
86 86 ]
87 87
88 88
89 89 class SimpleSvn(simplevcs.SimpleVCS):
90 90
91 91 SCM = 'svn'
92 92 READ_ONLY_COMMANDS = ('OPTIONS', 'PROPFIND', 'GET', 'REPORT')
93 93
94 94 def _get_repository_name(self, environ):
95 95 """
96 96 Gets repository name out of PATH_INFO header
97 97
98 98 :param environ: environ where PATH_INFO is stored
99 99 """
100 100 path = environ['PATH_INFO'].split('!')
101 101 repo_name = path[0].strip('/')
102 102
103 103 # SVN includes the whole path in it's requests, including
104 104 # subdirectories inside the repo. Therefore we have to search for
105 105 # the repo root directory.
106 106 if not is_valid_repo(repo_name, self.basepath, self.SCM):
107 107 current_path = ''
108 108 for component in repo_name.split('/'):
109 109 current_path += component
110 110 if is_valid_repo(current_path, self.basepath, self.SCM):
111 111 return current_path
112 112 current_path += '/'
113 113
114 114 return repo_name
115 115
116 116 def _get_action(self, environ):
117 117 return (
118 118 'pull'
119 119 if environ['REQUEST_METHOD'] in self.READ_ONLY_COMMANDS
120 120 else 'push')
121 121
122 122 def _create_wsgi_app(self, repo_path, repo_name, config):
123 123 return SimpleSvnApp(config)
124 124
125 125 def _create_config(self, extras, repo_name):
126 126 server_url = rhodecode.CONFIG.get(
127 127 'rhodecode_subversion_http_server_url', '')
128 128 extras['subversion_http_server_url'] = (
129 129 server_url or 'http://localhost/')
130 130 return extras
@@ -1,185 +1,185 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2010-2016 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 from StringIO import StringIO
22 22
23 23 import pytest
24 24 from mock import patch, Mock
25 25
26 26 import rhodecode
27 27 from rhodecode.lib.middleware.simplesvn import SimpleSvn, SimpleSvnApp
28 28
29 29
30 30 class TestSimpleSvn(object):
31 31 @pytest.fixture(autouse=True)
32 32 def simple_svn(self, pylonsapp):
33 33 self.app = SimpleSvn(
34 34 application='None',
35 35 config={'auth_ret_code': '',
36 36 'base_path': rhodecode.CONFIG['base_path']})
37 37
38 38 def test_get_config(self):
39 39 extras = {'foo': 'FOO', 'bar': 'BAR'}
40 40 config = self.app._create_config(extras, repo_name='test-repo')
41 41 assert config == extras
42 42
43 43 @pytest.mark.parametrize(
44 44 'method', ['OPTIONS', 'PROPFIND', 'GET', 'REPORT'])
45 45 def test_get_action_returns_pull(self, method):
46 46 environment = {'REQUEST_METHOD': method}
47 47 action = self.app._get_action(environment)
48 48 assert action == 'pull'
49 49
50 50 @pytest.mark.parametrize(
51 51 'method', [
52 52 'MKACTIVITY', 'PROPPATCH', 'PUT', 'CHECKOUT', 'MKCOL', 'MOVE',
53 53 'COPY', 'DELETE', 'LOCK', 'UNLOCK', 'MERGE'
54 54 ])
55 55 def test_get_action_returns_push(self, method):
56 56 environment = {'REQUEST_METHOD': method}
57 57 action = self.app._get_action(environment)
58 58 assert action == 'push'
59 59
60 60 @pytest.mark.parametrize(
61 61 'path, expected_name', [
62 62 ('/hello-svn', 'hello-svn'),
63 63 ('/hello-svn/', 'hello-svn'),
64 64 ('/group/hello-svn/', 'group/hello-svn'),
65 65 ('/group/hello-svn/!svn/vcc/default', 'group/hello-svn'),
66 66 ])
67 67 def test_get_repository_name(self, path, expected_name):
68 68 environment = {'PATH_INFO': path}
69 69 name = self.app._get_repository_name(environment)
70 70 assert name == expected_name
71 71
72 72 def test_get_repository_name_subfolder(self, backend_svn):
73 73 repo = backend_svn.repo
74 74 environment = {
75 75 'PATH_INFO': '/{}/path/with/subfolders'.format(repo.repo_name)}
76 76 name = self.app._get_repository_name(environment)
77 77 assert name == repo.repo_name
78 78
79 79 def test_create_wsgi_app(self):
80 80 with patch('rhodecode.lib.middleware.simplesvn.SimpleSvnApp') as (
81 81 wsgi_app_mock):
82 82 config = Mock()
83 83 wsgi_app = self.app._create_wsgi_app(
84 84 repo_path='', repo_name='', config=config)
85 85
86 86 wsgi_app_mock.assert_called_once_with(config)
87 87 assert wsgi_app == wsgi_app_mock()
88 88
89 89
90 90 class TestSimpleSvnApp(object):
91 91 data = '<xml></xml>'
92 92 path = '/group/my-repo'
93 93 wsgi_input = StringIO(data)
94 94 environment = {
95 95 'HTTP_DAV': (
96 96 'http://subversion.tigris.org/xmlns/dav/svn/depth,'
97 97 ' http://subversion.tigris.org/xmlns/dav/svn/mergeinfo'),
98 98 'HTTP_USER_AGENT': 'SVN/1.8.11 (x86_64-linux) serf/1.3.8',
99 99 'REQUEST_METHOD': 'OPTIONS',
100 100 'PATH_INFO': path,
101 101 'wsgi.input': wsgi_input,
102 102 'CONTENT_TYPE': 'text/xml',
103 103 'CONTENT_LENGTH': '130'
104 104 }
105 105
106 106 def setup_method(self, method):
107 107 self.host = 'http://localhost/'
108 108 self.app = SimpleSvnApp(
109 109 config={'subversion_http_server_url': self.host})
110 110
111 111 def test_get_request_headers_with_content_type(self):
112 112 expected_headers = {
113 113 'Dav': self.environment['HTTP_DAV'],
114 114 'User-Agent': self.environment['HTTP_USER_AGENT'],
115 115 'Content-Type': self.environment['CONTENT_TYPE'],
116 116 'Content-Length': self.environment['CONTENT_LENGTH']
117 117 }
118 118 headers = self.app._get_request_headers(self.environment)
119 119 assert headers == expected_headers
120 120
121 121 def test_get_request_headers_without_content_type(self):
122 122 environment = self.environment.copy()
123 123 environment.pop('CONTENT_TYPE')
124 124 expected_headers = {
125 125 'Dav': environment['HTTP_DAV'],
126 126 'Content-Length': self.environment['CONTENT_LENGTH'],
127 127 'User-Agent': environment['HTTP_USER_AGENT'],
128 128 }
129 129 request_headers = self.app._get_request_headers(environment)
130 130 assert request_headers == expected_headers
131 131
132 132 def test_get_response_headers(self):
133 133 headers = {
134 134 'Connection': 'keep-alive',
135 135 'Keep-Alive': 'timeout=5, max=100',
136 136 'Transfer-Encoding': 'chunked',
137 137 'Content-Encoding': 'gzip',
138 138 'MS-Author-Via': 'DAV',
139 139 'SVN-Supported-Posts': 'create-txn-with-props'
140 140 }
141 141 expected_headers = [
142 142 ('MS-Author-Via', 'DAV'),
143 143 ('SVN-Supported-Posts', 'create-txn-with-props')
144 144 ]
145 145 response_headers = self.app._get_response_headers(headers)
146 146 assert sorted(response_headers) == sorted(expected_headers)
147 147
148 148 def test_get_url(self):
149 149 url = self.app._get_url(self.path)
150 150 expected_url = '{}{}'.format(self.host.strip('/'), self.path)
151 151 assert url == expected_url
152 152
153 153 def test_call(self):
154 154 start_response = Mock()
155 155 response_mock = Mock()
156 156 response_mock.headers = {
157 157 'Content-Encoding': 'gzip',
158 158 'MS-Author-Via': 'DAV',
159 159 'SVN-Supported-Posts': 'create-txn-with-props'
160 160 }
161 161 response_mock.status_code = 200
162 162 response_mock.reason = 'OK'
163 163 with patch('rhodecode.lib.middleware.simplesvn.requests.request') as (
164 164 request_mock):
165 165 request_mock.return_value = response_mock
166 166 self.app(self.environment, start_response)
167 167
168 168 expected_url = '{}{}'.format(self.host.strip('/'), self.path)
169 169 expected_request_headers = {
170 170 'Dav': self.environment['HTTP_DAV'],
171 171 'User-Agent': self.environment['HTTP_USER_AGENT'],
172 172 'Content-Type': self.environment['CONTENT_TYPE'],
173 173 'Content-Length': self.environment['CONTENT_LENGTH']
174 174 }
175 175 expected_response_headers = [
176 176 ('SVN-Supported-Posts', 'create-txn-with-props'),
177 177 ('MS-Author-Via', 'DAV')
178 178 ]
179 179 request_mock.assert_called_once_with(
180 180 self.environment['REQUEST_METHOD'], expected_url,
181 data=self.wsgi_input, headers=expected_request_headers)
181 data=self.data, headers=expected_request_headers)
182 182 response_mock.iter_content.assert_called_once_with(chunk_size=1024)
183 183 args, _ = start_response.call_args
184 184 assert args[0] == '200 OK'
185 185 assert sorted(args[1]) == sorted(expected_response_headers)
General Comments 0
You need to be logged in to leave comments. Login now