##// END OF EJS Templates
chore(debug): improved logging for simplesvn
super-admin -
r5399:9679a5bf default
parent child Browse files
Show More
@@ -1,242 +1,242 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 import ConfigGet
30 from rhodecode import ConfigGet
31 from rhodecode.lib import rc_cache
31 from rhodecode.lib import rc_cache
32 from rhodecode.lib.middleware import simplevcs
32 from rhodecode.lib.middleware import simplevcs
33 from rhodecode.lib.middleware.utils import get_path_info
33 from rhodecode.lib.middleware.utils import get_path_info
34 from rhodecode.lib.utils import is_valid_repo
34 from rhodecode.lib.utils import is_valid_repo
35 from rhodecode.lib.str_utils import safe_str, safe_int, safe_bytes
35 from rhodecode.lib.str_utils import safe_str, safe_int, safe_bytes
36 from rhodecode.lib.type_utils import str2bool
36 from rhodecode.lib.type_utils import str2bool
37 from rhodecode.lib.ext_json import json
37 from rhodecode.lib.ext_json import json
38 from rhodecode.lib.hook_daemon.base import store_txn_id_data
38 from rhodecode.lib.hook_daemon.base import store_txn_id_data
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 self.session = requests.Session()
51 self.session = requests.Session()
52
52
53 def __call__(self, environ, start_response):
53 def __call__(self, environ, start_response):
54 request_headers = self._get_request_headers(environ)
54 request_headers = self._get_request_headers(environ)
55 data_io = environ['wsgi.input']
55 data_io = environ['wsgi.input']
56 req_method: str = environ['REQUEST_METHOD']
56 req_method: str = environ['REQUEST_METHOD']
57 has_content_length = 'CONTENT_LENGTH' in environ
57 has_content_length: bool = 'CONTENT_LENGTH' in environ
58
58
59 path_info = self._get_url(
59 path_info = self._get_url(
60 self.config.get('subversion_http_server_url', ''), get_path_info(environ))
60 self.config.get('subversion_http_server_url', ''), get_path_info(environ))
61 transfer_encoding = environ.get('HTTP_TRANSFER_ENCODING', '')
61 transfer_encoding = environ.get('HTTP_TRANSFER_ENCODING', '')
62 log.debug('Handling: %s method via `%s`', req_method, path_info)
62 log.debug('Handling: %s method via `%s` has_content_length:%s', req_method, path_info, has_content_length)
63
63
64 # stream control flag, based on request and content type...
64 # stream control flag, based on request and content type...
65 stream = False
65 stream = False
66
66
67 if req_method in ['MKCOL'] or has_content_length:
67 if req_method in ['MKCOL'] or has_content_length:
68 data_processed = False
68 data_processed = False
69 # read chunk to check if we have txn-with-props
69 # read chunk to check if we have txn-with-props
70 initial_data: bytes = data_io.read(1024)
70 initial_data: bytes = data_io.read(1024)
71 if initial_data.startswith(b'(create-txn-with-props'):
71 if initial_data.startswith(b'(create-txn-with-props'):
72 data_io = initial_data + data_io.read()
72 data_io = initial_data + data_io.read()
73 # store on-the-fly our rc_extra using svn revision properties
73 # store on-the-fly our rc_extra using svn revision properties
74 # those can be read later on in hooks executed so we have a way
74 # those can be read later on in hooks executed so we have a way
75 # to pass in the data into svn hooks
75 # to pass in the data into svn hooks
76 rc_data = base64.urlsafe_b64encode(json.dumps(self.rc_extras))
76 rc_data = base64.urlsafe_b64encode(json.dumps(self.rc_extras))
77 rc_data_len = str(len(rc_data))
77 rc_data_len = str(len(rc_data))
78 # header defines data length, and serialized data
78 # header defines data length, and serialized data
79 skel = b' rc-scm-extras %b %b' % (safe_bytes(rc_data_len), safe_bytes(rc_data))
79 skel = b' rc-scm-extras %b %b' % (safe_bytes(rc_data_len), safe_bytes(rc_data))
80 data_io = data_io[:-2] + skel + b'))'
80 data_io = data_io[:-2] + skel + b'))'
81 data_processed = True
81 data_processed = True
82
82
83 if not data_processed:
83 if not data_processed:
84 # NOTE(johbo): Avoid that we end up with sending the request in chunked
84 # NOTE(johbo): Avoid that we end up with sending the request in chunked
85 # transfer encoding (mainly on Gunicorn). If we know the content
85 # transfer encoding (mainly on Gunicorn). If we know the content
86 # length, then we should transfer the payload in one request.
86 # length, then we should transfer the payload in one request.
87 data_io = initial_data + data_io.read()
87 data_io = initial_data + data_io.read()
88
88
89 if req_method in ['GET', 'PUT'] or transfer_encoding == 'chunked':
89 if req_method in ['GET', 'PUT'] or transfer_encoding == 'chunked':
90 # NOTE(marcink): when getting/uploading files, we want to STREAM content
90 # NOTE(marcink): when getting/uploading files, we want to STREAM content
91 # back to the client/proxy instead of buffering it here...
91 # back to the client/proxy instead of buffering it here...
92 stream = True
92 stream = True
93
93
94 stream = stream
94 stream = stream
95 log.debug('Calling SVN PROXY at `%s`, using method:%s. Stream: %s',
95 log.debug('Calling SVN PROXY at `%s`, using method:%s. Stream: %s',
96 path_info, req_method, stream)
96 path_info, req_method, stream)
97
97
98 call_kwargs = dict(
98 call_kwargs = dict(
99 data=data_io,
99 data=data_io,
100 headers=request_headers,
100 headers=request_headers,
101 stream=stream
101 stream=stream
102 )
102 )
103 if req_method in ['HEAD', 'DELETE']:
103 if req_method in ['HEAD', 'DELETE']:
104 del call_kwargs['data']
104 del call_kwargs['data']
105
105
106 try:
106 try:
107 response = self.session.request(
107 response = self.session.request(
108 req_method, path_info, **call_kwargs)
108 req_method, path_info, **call_kwargs)
109 except requests.ConnectionError:
109 except requests.ConnectionError:
110 log.exception('ConnectionError occurred for endpoint %s', path_info)
110 log.exception('ConnectionError occurred for endpoint %s', path_info)
111 raise
111 raise
112
112
113 if response.status_code not in [200, 401]:
113 if response.status_code not in [200, 401]:
114 text = '\n{}'.format(safe_str(response.text)) if response.text else ''
114 text = '\n{}'.format(safe_str(response.text)) if response.text else ''
115 if response.status_code >= 500:
115 if response.status_code >= 500:
116 log.error('Got SVN response:%s with text:`%s`', response, text)
116 log.error('Got SVN response:%s with text:`%s`', response, text)
117 else:
117 else:
118 log.debug('Got SVN response:%s with text:`%s`', response, text)
118 log.debug('Got SVN response:%s with text:`%s`', response, text)
119 else:
119 else:
120 log.debug('got response code: %s', response.status_code)
120 log.debug('got response code: %s', response.status_code)
121
121
122 response_headers = self._get_response_headers(response.headers)
122 response_headers = self._get_response_headers(response.headers)
123
123
124 if response.headers.get('SVN-Txn-name'):
124 if response.headers.get('SVN-Txn-name'):
125 svn_tx_id = response.headers.get('SVN-Txn-name')
125 svn_tx_id = response.headers.get('SVN-Txn-name')
126 txn_id = rc_cache.utils.compute_key_from_params(
126 txn_id = rc_cache.utils.compute_key_from_params(
127 self.config['repository'], svn_tx_id)
127 self.config['repository'], svn_tx_id)
128 port = safe_int(self.rc_extras['hooks_uri'].split(':')[-1])
128 port = safe_int(self.rc_extras['hooks_uri'].split(':')[-1])
129 store_txn_id_data(txn_id, {'port': port})
129 store_txn_id_data(txn_id, {'port': port})
130
130
131 start_response(f'{response.status_code} {response.reason}', response_headers)
131 start_response(f'{response.status_code} {response.reason}', response_headers)
132 return response.iter_content(chunk_size=1024)
132 return response.iter_content(chunk_size=1024)
133
133
134 def _get_url(self, svn_http_server, path):
134 def _get_url(self, svn_http_server, path):
135 svn_http_server_url = (svn_http_server or '').rstrip('/')
135 svn_http_server_url = (svn_http_server or '').rstrip('/')
136 url_path = urllib.parse.urljoin(svn_http_server_url + '/', (path or '').lstrip('/'))
136 url_path = urllib.parse.urljoin(svn_http_server_url + '/', (path or '').lstrip('/'))
137 url_path = urllib.parse.quote(url_path, safe="/:=~+!$,;'")
137 url_path = urllib.parse.quote(url_path, safe="/:=~+!$,;'")
138 return url_path
138 return url_path
139
139
140 def _get_request_headers(self, environ):
140 def _get_request_headers(self, environ):
141 headers = {}
141 headers = {}
142 whitelist = {
142 whitelist = {
143 'Authorization': {}
143 'Authorization': {}
144 }
144 }
145 for key in environ:
145 for key in environ:
146 if key in whitelist:
146 if key in whitelist:
147 headers[key] = environ[key]
147 headers[key] = environ[key]
148 elif not key.startswith('HTTP_'):
148 elif not key.startswith('HTTP_'):
149 continue
149 continue
150 else:
150 else:
151 new_key = key.split('_')
151 new_key = key.split('_')
152 new_key = [k.capitalize() for k in new_key[1:]]
152 new_key = [k.capitalize() for k in new_key[1:]]
153 new_key = '-'.join(new_key)
153 new_key = '-'.join(new_key)
154 headers[new_key] = environ[key]
154 headers[new_key] = environ[key]
155
155
156 if 'CONTENT_TYPE' in environ:
156 if 'CONTENT_TYPE' in environ:
157 headers['Content-Type'] = environ['CONTENT_TYPE']
157 headers['Content-Type'] = environ['CONTENT_TYPE']
158
158
159 if 'CONTENT_LENGTH' in environ:
159 if 'CONTENT_LENGTH' in environ:
160 headers['Content-Length'] = environ['CONTENT_LENGTH']
160 headers['Content-Length'] = environ['CONTENT_LENGTH']
161
161
162 return headers
162 return headers
163
163
164 def _get_response_headers(self, headers):
164 def _get_response_headers(self, headers):
165 headers = [
165 headers = [
166 (h, headers[h])
166 (h, headers[h])
167 for h in headers
167 for h in headers
168 if h.lower() not in self.IGNORED_HEADERS
168 if h.lower() not in self.IGNORED_HEADERS
169 ]
169 ]
170
170
171 return headers
171 return headers
172
172
173
173
174 class DisabledSimpleSvnApp(object):
174 class DisabledSimpleSvnApp(object):
175 def __init__(self, config):
175 def __init__(self, config):
176 self.config = config
176 self.config = config
177
177
178 def __call__(self, environ, start_response):
178 def __call__(self, environ, start_response):
179 reason = 'Cannot handle SVN call because: SVN HTTP Proxy is not enabled'
179 reason = 'Cannot handle SVN call because: SVN HTTP Proxy is not enabled'
180 log.warning(reason)
180 log.warning(reason)
181 return HTTPNotAcceptable(reason)(environ, start_response)
181 return HTTPNotAcceptable(reason)(environ, start_response)
182
182
183
183
184 class SimpleSvn(simplevcs.SimpleVCS):
184 class SimpleSvn(simplevcs.SimpleVCS):
185
185
186 SCM = 'svn'
186 SCM = 'svn'
187 READ_ONLY_COMMANDS = ('OPTIONS', 'PROPFIND', 'GET', 'REPORT')
187 READ_ONLY_COMMANDS = ('OPTIONS', 'PROPFIND', 'GET', 'REPORT')
188 DEFAULT_HTTP_SERVER = 'http://localhost:8090'
188 DEFAULT_HTTP_SERVER = 'http://localhost:8090'
189
189
190 def _get_repository_name(self, environ):
190 def _get_repository_name(self, environ):
191 """
191 """
192 Gets repository name out of PATH_INFO header
192 Gets repository name out of PATH_INFO header
193
193
194 :param environ: environ where PATH_INFO is stored
194 :param environ: environ where PATH_INFO is stored
195 """
195 """
196 path = get_path_info(environ).split('!')
196 path = get_path_info(environ).split('!')
197 repo_name = path[0].strip('/')
197 repo_name = path[0].strip('/')
198
198
199 # SVN includes the whole path in it's requests, including
199 # SVN includes the whole path in it's requests, including
200 # subdirectories inside the repo. Therefore we have to search for
200 # subdirectories inside the repo. Therefore we have to search for
201 # the repo root directory.
201 # the repo root directory.
202 if not is_valid_repo(
202 if not is_valid_repo(
203 repo_name, self.base_path, explicit_scm=self.SCM):
203 repo_name, self.base_path, explicit_scm=self.SCM):
204 current_path = ''
204 current_path = ''
205 for component in repo_name.split('/'):
205 for component in repo_name.split('/'):
206 current_path += component
206 current_path += component
207 if is_valid_repo(
207 if is_valid_repo(
208 current_path, self.base_path, explicit_scm=self.SCM):
208 current_path, self.base_path, explicit_scm=self.SCM):
209 return current_path
209 return current_path
210 current_path += '/'
210 current_path += '/'
211
211
212 return repo_name
212 return repo_name
213
213
214 def _get_action(self, environ):
214 def _get_action(self, environ):
215 return (
215 return (
216 'pull'
216 'pull'
217 if environ['REQUEST_METHOD'] in self.READ_ONLY_COMMANDS
217 if environ['REQUEST_METHOD'] in self.READ_ONLY_COMMANDS
218 else 'push')
218 else 'push')
219
219
220 def _should_use_callback_daemon(self, extras, environ, action):
220 def _should_use_callback_daemon(self, extras, environ, action):
221 # only MERGE command triggers hooks, so we don't want to start
221 # only MERGE command triggers hooks, so we don't want to start
222 # hooks server too many times. POST however starts the svn transaction
222 # hooks server too many times. POST however starts the svn transaction
223 # so we also need to run the init of callback daemon of POST
223 # so we also need to run the init of callback daemon of POST
224 if environ['REQUEST_METHOD'] in ['MERGE', 'POST']:
224 if environ['REQUEST_METHOD'] in ['MERGE', 'POST']:
225 return True
225 return True
226 return False
226 return False
227
227
228 def _create_wsgi_app(self, repo_path, repo_name, config):
228 def _create_wsgi_app(self, repo_path, repo_name, config):
229 if self._is_svn_enabled():
229 if self._is_svn_enabled():
230 return SimpleSvnApp(config)
230 return SimpleSvnApp(config)
231 # we don't have http proxy enabled return dummy request handler
231 # we don't have http proxy enabled return dummy request handler
232 return DisabledSimpleSvnApp(config)
232 return DisabledSimpleSvnApp(config)
233
233
234 def _is_svn_enabled(self):
234 def _is_svn_enabled(self):
235 return ConfigGet().get_bool('vcs.svn.proxy.enabled')
235 return ConfigGet().get_bool('vcs.svn.proxy.enabled')
236
236
237 def _create_config(self, extras, repo_name, scheme='http'):
237 def _create_config(self, extras, repo_name, scheme='http'):
238 server_url = ConfigGet().get_str('vcs.svn.proxy.host')
238 server_url = ConfigGet().get_str('vcs.svn.proxy.host')
239 server_url = server_url or self.DEFAULT_HTTP_SERVER
239 server_url = server_url or self.DEFAULT_HTTP_SERVER
240
240
241 extras['subversion_http_server_url'] = server_url
241 extras['subversion_http_server_url'] = server_url
242 return extras
242 return extras
General Comments 0
You need to be logged in to leave comments. Login now