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