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