##// END OF EJS Templates
svn: fixed svn operations
super-admin -
r5156:a1eaf428 default
parent child Browse files
Show More
@@ -1,232 +1,231 b''
1
1
2 # Copyright (C) 2010-2023 RhodeCode GmbH
2 # Copyright (C) 2010-2023 RhodeCode GmbH
3 #
3 #
4 # This program is free software: you can redistribute it and/or modify
4 # This program is free software: you can redistribute it and/or modify
5 # it under the terms of the GNU Affero General Public License, version 3
5 # it under the terms of the GNU Affero General Public License, version 3
6 # (only), as published by the Free Software Foundation.
6 # (only), as published by the Free Software Foundation.
7 #
7 #
8 # This program is distributed in the hope that it will be useful,
8 # This program is distributed in the hope that it will be useful,
9 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # GNU General Public License for more details.
11 # GNU General Public License for more details.
12 #
12 #
13 # You should have received a copy of the GNU Affero General Public License
13 # You should have received a copy of the GNU Affero General Public License
14 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 #
15 #
16 # This program is dual-licensed. If you wish to learn more about the
16 # This program is dual-licensed. If you wish to learn more about the
17 # RhodeCode Enterprise Edition, including its added features, Support services,
17 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # and proprietary license terms, please see https://rhodecode.com/licenses/
18 # and proprietary license terms, please see https://rhodecode.com/licenses/
19
19
20 import base64
20 import base64
21 import logging
21 import logging
22 import urllib.request
22 import urllib.request
23 import urllib.parse
23 import urllib.parse
24 import urllib.error
24 import urllib.error
25 import urllib.parse
25 import urllib.parse
26
26
27 import requests
27 import requests
28 from pyramid.httpexceptions import HTTPNotAcceptable
28 from pyramid.httpexceptions import HTTPNotAcceptable
29
29
30 from rhodecode.lib import rc_cache
30 from rhodecode.lib import rc_cache
31 from rhodecode.lib.middleware import simplevcs
31 from rhodecode.lib.middleware import simplevcs
32 from rhodecode.lib.middleware.utils import get_path_info
32 from rhodecode.lib.middleware.utils import get_path_info
33 from rhodecode.lib.utils import is_valid_repo
33 from rhodecode.lib.utils import is_valid_repo
34 from rhodecode.lib.str_utils import safe_str, safe_int
34 from rhodecode.lib.str_utils import safe_str, safe_int, safe_bytes
35 from rhodecode.lib.type_utils import str2bool
35 from rhodecode.lib.type_utils import str2bool
36 from rhodecode.lib.ext_json import json
36 from rhodecode.lib.ext_json import json
37 from rhodecode.lib.hooks_daemon import store_txn_id_data
37 from rhodecode.lib.hooks_daemon import store_txn_id_data
38
38
39
39
40 log = logging.getLogger(__name__)
40 log = logging.getLogger(__name__)
41
41
42
42
43 class SimpleSvnApp(object):
43 class SimpleSvnApp(object):
44 IGNORED_HEADERS = [
44 IGNORED_HEADERS = [
45 'connection', 'keep-alive', 'content-encoding',
45 'connection', 'keep-alive', 'content-encoding',
46 'transfer-encoding', 'content-length']
46 'transfer-encoding', 'content-length']
47 rc_extras = {}
47 rc_extras = {}
48
48
49 def __init__(self, config):
49 def __init__(self, config):
50 self.config = config
50 self.config = config
51
51
52 def __call__(self, environ, start_response):
52 def __call__(self, environ, start_response):
53 request_headers = self._get_request_headers(environ)
53 request_headers = self._get_request_headers(environ)
54 data = environ['wsgi.input']
54 data_io = environ['wsgi.input']
55 req_method = environ['REQUEST_METHOD']
55 req_method: str = environ['REQUEST_METHOD']
56 has_content_length = 'CONTENT_LENGTH' in environ
56 has_content_length = 'CONTENT_LENGTH' in environ
57
57
58 path_info = self._get_url(
58 path_info = self._get_url(
59 self.config.get('subversion_http_server_url', ''), get_path_info(environ))
59 self.config.get('subversion_http_server_url', ''), get_path_info(environ))
60 transfer_encoding = environ.get('HTTP_TRANSFER_ENCODING', '')
60 transfer_encoding = environ.get('HTTP_TRANSFER_ENCODING', '')
61 log.debug('Handling: %s method via `%s`', req_method, path_info)
61 log.debug('Handling: %s method via `%s`', req_method, path_info)
62
62
63 # stream control flag, based on request and content type...
63 # stream control flag, based on request and content type...
64 stream = False
64 stream = False
65
65
66 if req_method in ['MKCOL'] or has_content_length:
66 if req_method in ['MKCOL'] or has_content_length:
67 data_processed = False
67 data_processed = False
68 # read chunk to check if we have txn-with-props
68 # read chunk to check if we have txn-with-props
69 initial_data = data.read(1024)
69 initial_data: bytes = data_io.read(1024)
70 if initial_data.startswith('(create-txn-with-props'):
70 if initial_data.startswith(b'(create-txn-with-props'):
71 data = initial_data + data.read()
71 data_io = initial_data + data_io.read()
72 # store on-the-fly our rc_extra using svn revision properties
72 # store on-the-fly our rc_extra using svn revision properties
73 # those can be read later on in hooks executed so we have a way
73 # those can be read later on in hooks executed so we have a way
74 # to pass in the data into svn hooks
74 # to pass in the data into svn hooks
75 rc_data = base64.urlsafe_b64encode(json.dumps(self.rc_extras))
75 rc_data = base64.urlsafe_b64encode(json.dumps(self.rc_extras))
76 rc_data_len = len(rc_data)
76 rc_data_len = str(len(rc_data))
77 # header defines data length, and serialized data
77 # header defines data length, and serialized data
78 skel = ' rc-scm-extras {} {}'.format(rc_data_len, rc_data)
78 skel = b' rc-scm-extras %b %b' % (safe_bytes(rc_data_len), safe_bytes(rc_data))
79 data = data[:-2] + skel + '))'
79 data_io = data_io[:-2] + skel + b'))'
80 data_processed = True
80 data_processed = True
81
81
82 if not data_processed:
82 if not data_processed:
83 # NOTE(johbo): Avoid that we end up with sending the request in chunked
83 # NOTE(johbo): Avoid that we end up with sending the request in chunked
84 # transfer encoding (mainly on Gunicorn). If we know the content
84 # transfer encoding (mainly on Gunicorn). If we know the content
85 # length, then we should transfer the payload in one request.
85 # length, then we should transfer the payload in one request.
86 data = initial_data + data.read()
86 data_io = initial_data + data_io.read()
87
87
88 if req_method in ['GET', 'PUT'] or transfer_encoding == 'chunked':
88 if req_method in ['GET', 'PUT'] or transfer_encoding == 'chunked':
89 # NOTE(marcink): when getting/uploading files we want to STREAM content
89 # NOTE(marcink): when getting/uploading files, we want to STREAM content
90 # back to the client/proxy instead of buffering it here...
90 # back to the client/proxy instead of buffering it here...
91 stream = True
91 stream = True
92
92
93 stream = stream
93 stream = stream
94 log.debug('Calling SVN PROXY at `%s`, using method:%s. Stream: %s',
94 log.debug('Calling SVN PROXY at `%s`, using method:%s. Stream: %s',
95 path_info, req_method, stream)
95 path_info, req_method, stream)
96
96 try:
97 try:
97 response = requests.request(
98 response = requests.request(
98 req_method, path_info,
99 req_method, path_info,
99 data=data, headers=request_headers, stream=stream)
100 data=data_io, headers=request_headers, stream=stream)
100 except requests.ConnectionError:
101 except requests.ConnectionError:
101 log.exception('ConnectionError occurred for endpoint %s', path_info)
102 log.exception('ConnectionError occurred for endpoint %s', path_info)
102 raise
103 raise
103
104
104 if response.status_code not in [200, 401]:
105 if response.status_code not in [200, 401]:
105 text = '\n{}'.format(safe_str(response.text)) if response.text else ''
106 text = '\n{}'.format(safe_str(response.text)) if response.text else ''
106 if response.status_code >= 500:
107 if response.status_code >= 500:
107 log.error('Got SVN response:%s with text:`%s`', response, text)
108 log.error('Got SVN response:%s with text:`%s`', response, text)
108 else:
109 else:
109 log.debug('Got SVN response:%s with text:`%s`', response, text)
110 log.debug('Got SVN response:%s with text:`%s`', response, text)
110 else:
111 else:
111 log.debug('got response code: %s', response.status_code)
112 log.debug('got response code: %s', response.status_code)
112
113
113 response_headers = self._get_response_headers(response.headers)
114 response_headers = self._get_response_headers(response.headers)
114
115
115 if response.headers.get('SVN-Txn-name'):
116 if response.headers.get('SVN-Txn-name'):
116 svn_tx_id = response.headers.get('SVN-Txn-name')
117 svn_tx_id = response.headers.get('SVN-Txn-name')
117 txn_id = rc_cache.utils.compute_key_from_params(
118 txn_id = rc_cache.utils.compute_key_from_params(
118 self.config['repository'], svn_tx_id)
119 self.config['repository'], svn_tx_id)
119 port = safe_int(self.rc_extras['hooks_uri'].split(':')[-1])
120 port = safe_int(self.rc_extras['hooks_uri'].split(':')[-1])
120 store_txn_id_data(txn_id, {'port': port})
121 store_txn_id_data(txn_id, {'port': port})
121
122
122 start_response(
123 start_response(f'{response.status_code} {response.reason}', response_headers)
123 '{} {}'.format(response.status_code, response.reason),
124 response_headers)
125 return response.iter_content(chunk_size=1024)
124 return response.iter_content(chunk_size=1024)
126
125
127 def _get_url(self, svn_http_server, path):
126 def _get_url(self, svn_http_server, path):
128 svn_http_server_url = (svn_http_server or '').rstrip('/')
127 svn_http_server_url = (svn_http_server or '').rstrip('/')
129 url_path = urllib.parse.urljoin(svn_http_server_url + '/', (path or '').lstrip('/'))
128 url_path = urllib.parse.urljoin(svn_http_server_url + '/', (path or '').lstrip('/'))
130 url_path = urllib.parse.quote(url_path, safe="/:=~+!$,;'")
129 url_path = urllib.parse.quote(url_path, safe="/:=~+!$,;'")
131 return url_path
130 return url_path
132
131
133 def _get_request_headers(self, environ):
132 def _get_request_headers(self, environ):
134 headers = {}
133 headers = {}
135
134
136 for key in environ:
135 for key in environ:
137 if not key.startswith('HTTP_'):
136 if not key.startswith('HTTP_'):
138 continue
137 continue
139 new_key = key.split('_')
138 new_key = key.split('_')
140 new_key = [k.capitalize() for k in new_key[1:]]
139 new_key = [k.capitalize() for k in new_key[1:]]
141 new_key = '-'.join(new_key)
140 new_key = '-'.join(new_key)
142 headers[new_key] = environ[key]
141 headers[new_key] = environ[key]
143
142
144 if 'CONTENT_TYPE' in environ:
143 if 'CONTENT_TYPE' in environ:
145 headers['Content-Type'] = environ['CONTENT_TYPE']
144 headers['Content-Type'] = environ['CONTENT_TYPE']
146
145
147 if 'CONTENT_LENGTH' in environ:
146 if 'CONTENT_LENGTH' in environ:
148 headers['Content-Length'] = environ['CONTENT_LENGTH']
147 headers['Content-Length'] = environ['CONTENT_LENGTH']
149
148
150 return headers
149 return headers
151
150
152 def _get_response_headers(self, headers):
151 def _get_response_headers(self, headers):
153 headers = [
152 headers = [
154 (h, headers[h])
153 (h, headers[h])
155 for h in headers
154 for h in headers
156 if h.lower() not in self.IGNORED_HEADERS
155 if h.lower() not in self.IGNORED_HEADERS
157 ]
156 ]
158
157
159 return headers
158 return headers
160
159
161
160
162 class DisabledSimpleSvnApp(object):
161 class DisabledSimpleSvnApp(object):
163 def __init__(self, config):
162 def __init__(self, config):
164 self.config = config
163 self.config = config
165
164
166 def __call__(self, environ, start_response):
165 def __call__(self, environ, start_response):
167 reason = 'Cannot handle SVN call because: SVN HTTP Proxy is not enabled'
166 reason = 'Cannot handle SVN call because: SVN HTTP Proxy is not enabled'
168 log.warning(reason)
167 log.warning(reason)
169 return HTTPNotAcceptable(reason)(environ, start_response)
168 return HTTPNotAcceptable(reason)(environ, start_response)
170
169
171
170
172 class SimpleSvn(simplevcs.SimpleVCS):
171 class SimpleSvn(simplevcs.SimpleVCS):
173
172
174 SCM = 'svn'
173 SCM = 'svn'
175 READ_ONLY_COMMANDS = ('OPTIONS', 'PROPFIND', 'GET', 'REPORT')
174 READ_ONLY_COMMANDS = ('OPTIONS', 'PROPFIND', 'GET', 'REPORT')
176 DEFAULT_HTTP_SERVER = 'http://localhost:8090'
175 DEFAULT_HTTP_SERVER = 'http://localhost:8090'
177
176
178 def _get_repository_name(self, environ):
177 def _get_repository_name(self, environ):
179 """
178 """
180 Gets repository name out of PATH_INFO header
179 Gets repository name out of PATH_INFO header
181
180
182 :param environ: environ where PATH_INFO is stored
181 :param environ: environ where PATH_INFO is stored
183 """
182 """
184 path = get_path_info(environ).split('!')
183 path = get_path_info(environ).split('!')
185 repo_name = path[0].strip('/')
184 repo_name = path[0].strip('/')
186
185
187 # SVN includes the whole path in it's requests, including
186 # SVN includes the whole path in it's requests, including
188 # subdirectories inside the repo. Therefore we have to search for
187 # subdirectories inside the repo. Therefore we have to search for
189 # the repo root directory.
188 # the repo root directory.
190 if not is_valid_repo(
189 if not is_valid_repo(
191 repo_name, self.base_path, explicit_scm=self.SCM):
190 repo_name, self.base_path, explicit_scm=self.SCM):
192 current_path = ''
191 current_path = ''
193 for component in repo_name.split('/'):
192 for component in repo_name.split('/'):
194 current_path += component
193 current_path += component
195 if is_valid_repo(
194 if is_valid_repo(
196 current_path, self.base_path, explicit_scm=self.SCM):
195 current_path, self.base_path, explicit_scm=self.SCM):
197 return current_path
196 return current_path
198 current_path += '/'
197 current_path += '/'
199
198
200 return repo_name
199 return repo_name
201
200
202 def _get_action(self, environ):
201 def _get_action(self, environ):
203 return (
202 return (
204 'pull'
203 'pull'
205 if environ['REQUEST_METHOD'] in self.READ_ONLY_COMMANDS
204 if environ['REQUEST_METHOD'] in self.READ_ONLY_COMMANDS
206 else 'push')
205 else 'push')
207
206
208 def _should_use_callback_daemon(self, extras, environ, action):
207 def _should_use_callback_daemon(self, extras, environ, action):
209 # only MERGE command triggers hooks, so we don't want to start
208 # only MERGE command triggers hooks, so we don't want to start
210 # hooks server too many times. POST however starts the svn transaction
209 # hooks server too many times. POST however starts the svn transaction
211 # so we also need to run the init of callback daemon of POST
210 # so we also need to run the init of callback daemon of POST
212 if environ['REQUEST_METHOD'] in ['MERGE', 'POST']:
211 if environ['REQUEST_METHOD'] in ['MERGE', 'POST']:
213 return True
212 return True
214 return False
213 return False
215
214
216 def _create_wsgi_app(self, repo_path, repo_name, config):
215 def _create_wsgi_app(self, repo_path, repo_name, config):
217 if self._is_svn_enabled():
216 if self._is_svn_enabled():
218 return SimpleSvnApp(config)
217 return SimpleSvnApp(config)
219 # we don't have http proxy enabled return dummy request handler
218 # we don't have http proxy enabled return dummy request handler
220 return DisabledSimpleSvnApp(config)
219 return DisabledSimpleSvnApp(config)
221
220
222 def _is_svn_enabled(self):
221 def _is_svn_enabled(self):
223 conf = self.repo_vcs_config
222 conf = self.repo_vcs_config
224 return str2bool(conf.get('vcs_svn_proxy', 'http_requests_enabled'))
223 return str2bool(conf.get('vcs_svn_proxy', 'http_requests_enabled'))
225
224
226 def _create_config(self, extras, repo_name, scheme='http'):
225 def _create_config(self, extras, repo_name, scheme='http'):
227 conf = self.repo_vcs_config
226 conf = self.repo_vcs_config
228 server_url = conf.get('vcs_svn_proxy', 'http_server_url')
227 server_url = conf.get('vcs_svn_proxy', 'http_server_url')
229 server_url = server_url or self.DEFAULT_HTTP_SERVER
228 server_url = server_url or self.DEFAULT_HTTP_SERVER
230
229
231 extras['subversion_http_server_url'] = server_url
230 extras['subversion_http_server_url'] = server_url
232 return extras
231 return extras
General Comments 0
You need to be logged in to leave comments. Login now