##// END OF EJS Templates
fix(vcs): fixed logic bug on skip-vcs detector where it didn't skip properly defined urls
super-admin -
r5533:ec540a35 default
parent child Browse files
Show More
@@ -1,308 +1,312 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 gzip
21 21 import shutil
22 22 import logging
23 23 import tempfile
24 24 import urllib.parse
25 25
26 26 from webob.exc import HTTPNotFound
27 27
28 28 import rhodecode
29 29 from rhodecode.apps._base import ADMIN_PREFIX
30 30 from rhodecode.lib.middleware.utils import get_path_info
31 31 from rhodecode.lib.middleware.appenlight import wrap_in_appenlight_if_enabled
32 32 from rhodecode.lib.middleware.simplegit import SimpleGit, GIT_PROTO_PAT
33 33 from rhodecode.lib.middleware.simplehg import SimpleHg
34 34 from rhodecode.lib.middleware.simplesvn import SimpleSvn
35 35 from rhodecode.lib.str_utils import safe_str
36 36 from rhodecode.model.settings import VcsSettingsModel
37 37
38 38
39 39 log = logging.getLogger(__name__)
40 40
41 41 VCS_TYPE_KEY = '_rc_vcs_type'
42 42 VCS_TYPE_SKIP = '_rc_vcs_skip'
43 43
44 44
45 45 def is_git(environ):
46 46 """
47 47 Returns True if requests should be handled by GIT wsgi middleware
48 48 """
49 49 path_info = get_path_info(environ)
50 50 is_git_path = GIT_PROTO_PAT.match(path_info)
51 51 log.debug(
52 52 'request path: `%s` detected as GIT PROTOCOL %s', path_info,
53 53 is_git_path is not None)
54 54
55 55 return is_git_path
56 56
57 57
58 58 def is_hg(environ):
59 59 """
60 60 Returns True if requests target is mercurial server - header
61 61 ``HTTP_ACCEPT`` of such request would start with ``application/mercurial``.
62 62 """
63 63 is_hg_path = False
64 64
65 65 http_accept = environ.get('HTTP_ACCEPT')
66 66
67 67 if http_accept and http_accept.startswith('application/mercurial'):
68 68 query = urllib.parse.parse_qs(environ['QUERY_STRING'])
69 69 if 'cmd' in query:
70 70 is_hg_path = True
71 71
72 72 path_info = get_path_info(environ)
73 73 log.debug(
74 74 'request path: `%s` detected as HG PROTOCOL %s', path_info,
75 75 is_hg_path)
76 76
77 77 return is_hg_path
78 78
79 79
80 80 def is_svn(environ):
81 81 """
82 82 Returns True if requests target is Subversion server
83 83 """
84 84
85 85 http_dav = environ.get('HTTP_DAV', '')
86 86 magic_path_segment = rhodecode.CONFIG.get(
87 87 'rhodecode_subversion_magic_path', '/!svn')
88 88 path_info = get_path_info(environ)
89 89 req_method = environ['REQUEST_METHOD']
90 90
91 91 is_svn_path = (
92 92 'subversion' in http_dav or
93 93 magic_path_segment in path_info
94 94 or req_method in ['PROPFIND', 'PROPPATCH', 'HEAD']
95 95 )
96 96 log.debug(
97 97 'request path: `%s` detected as SVN PROTOCOL %s', path_info,
98 98 is_svn_path)
99 99
100 100 return is_svn_path
101 101
102 102
103 103 class GunzipMiddleware(object):
104 104 """
105 105 WSGI middleware that unzips gzip-encoded requests before
106 106 passing on to the underlying application.
107 107 """
108 108
109 109 def __init__(self, application):
110 110 self.app = application
111 111
112 112 def __call__(self, environ, start_response):
113 113 accepts_encoding_header = safe_str(environ.get('HTTP_CONTENT_ENCODING', ''))
114 114
115 115 if 'gzip' in accepts_encoding_header:
116 116 log.debug('gzip detected, now running gunzip wrapper')
117 117 wsgi_input = environ['wsgi.input']
118 118
119 119 if not hasattr(environ['wsgi.input'], 'seek'):
120 120 # The gzip implementation in the standard library of Python 2.x
121 121 # requires the '.seek()' and '.tell()' methods to be available
122 122 # on the input stream. Read the data into a temporary file to
123 123 # work around this limitation.
124 124
125 125 wsgi_input = tempfile.SpooledTemporaryFile(64 * 1024 * 1024)
126 126 shutil.copyfileobj(environ['wsgi.input'], wsgi_input)
127 127 wsgi_input.seek(0)
128 128
129 129 environ['wsgi.input'] = gzip.GzipFile(fileobj=wsgi_input, mode='r')
130 130 # since we "Ungzipped" the content we say now it's no longer gzip
131 131 # content encoding
132 132 del environ['HTTP_CONTENT_ENCODING']
133 133
134 134 # content length has changes ? or i'm not sure
135 135 if 'CONTENT_LENGTH' in environ:
136 136 del environ['CONTENT_LENGTH']
137 137 else:
138 138 log.debug('content not gzipped, gzipMiddleware passing '
139 139 'request further')
140 140 return self.app(environ, start_response)
141 141
142 142
143 143 def is_vcs_call(environ):
144 144 if VCS_TYPE_KEY in environ:
145 145 raw_type = environ[VCS_TYPE_KEY]
146 146 return raw_type and raw_type != VCS_TYPE_SKIP
147 147 return False
148 148
149 149
150 150 def detect_vcs_request(environ, backends):
151 151 checks = {
152 152 'hg': (is_hg, SimpleHg),
153 153 'git': (is_git, SimpleGit),
154 154 'svn': (is_svn, SimpleSvn),
155 155 }
156 156 handler = None
157 157 # List of path views first chunk we don't do any checks
158 158 white_list = [
159 159 # favicon often requested by browsers
160 160 'favicon.ico',
161 161
162 # static files no detection
163 '_static++',
164
165 # debug-toolbar
166 '_debug_toolbar++',
167
162 168 # e.g /_file_store/download
163 169 '_file_store++',
164 170
165 171 # login
166 "_admin/login",
172 f"{ADMIN_PREFIX}/login",
173 f"{ADMIN_PREFIX}/logout",
167 174
168 175 # 2fa
169 176 f"{ADMIN_PREFIX}/check_2fa",
170 177 f"{ADMIN_PREFIX}/setup_2fa",
171 178
172 179 # _admin/api is safe too
173 180 f'{ADMIN_PREFIX}/api',
174 181
175 182 # _admin/gist is safe too
176 183 f'{ADMIN_PREFIX}/gists++',
177 184
178 185 # _admin/my_account is safe too
179 186 f'{ADMIN_PREFIX}/my_account++',
180 187
181 # static files no detection
182 '_static++',
183
184 # debug-toolbar
185 '_debug_toolbar++',
186
187 188 # skip ops ping, status
188 189 f'{ADMIN_PREFIX}/ops/ping',
189 190 f'{ADMIN_PREFIX}/ops/status',
190 191
191 192 # full channelstream connect should be VCS skipped
192 193 f'{ADMIN_PREFIX}/channelstream/connect',
193 194
194 195 '++/repo_creating_check'
195 196 ]
197
196 198 path_info = get_path_info(environ)
197 199 path_url = path_info.lstrip('/')
198 200 req_method = environ.get('REQUEST_METHOD')
199 201
200 202 for item in white_list:
203 item = item.lstrip('/')
204
201 205 if item.endswith('++') and path_url.startswith(item[:-2]):
202 206 log.debug('path `%s` in whitelist (match:%s), skipping...', path_url, item)
203 207 return handler
204 208 if item.startswith('++') and path_url.endswith(item[2:]):
205 209 log.debug('path `%s` in whitelist (match:%s), skipping...', path_url, item)
206 210 return handler
207 211 if item == path_url:
208 212 log.debug('path `%s` in whitelist (match:%s), skipping...', path_url, item)
209 213 return handler
210 214
211 215 if VCS_TYPE_KEY in environ:
212 216 raw_type = environ[VCS_TYPE_KEY]
213 217 if raw_type == VCS_TYPE_SKIP:
214 218 log.debug('got `skip` marker for vcs detection, skipping...')
215 219 return handler
216 220
217 221 _check, handler = checks.get(raw_type) or [None, None]
218 222 if handler:
219 223 log.debug('got handler:%s from environ', handler)
220 224
221 225 if not handler:
222 226 log.debug('request start: checking if request for `%s:%s` is of VCS type in order: %s',
223 227 req_method, path_url, backends)
224 228 for vcs_type in backends:
225 229 vcs_check, _handler = checks[vcs_type]
226 230 if vcs_check(environ):
227 231 log.debug('vcs handler found %s', _handler)
228 232 handler = _handler
229 233 break
230 234
231 235 return handler
232 236
233 237
234 238 class VCSMiddleware(object):
235 239
236 240 def __init__(self, app, registry, config, appenlight_client):
237 241 self.application = app
238 242 self.registry = registry
239 243 self.config = config
240 244 self.appenlight_client = appenlight_client
241 245 self.use_gzip = True
242 246 # order in which we check the middlewares, based on vcs.backends config
243 247 self.check_middlewares = config['vcs.backends']
244 248
245 249 def vcs_config(self, repo_name=None):
246 250 """
247 251 returns serialized VcsSettings
248 252 """
249 253 try:
250 254 return VcsSettingsModel(
251 255 repo=repo_name).get_ui_settings_as_config_obj()
252 256 except Exception:
253 257 pass
254 258
255 259 def wrap_in_gzip_if_enabled(self, app, config):
256 260 if self.use_gzip:
257 261 app = GunzipMiddleware(app)
258 262 return app
259 263
260 264 def _get_handler_app(self, environ):
261 265 app = None
262 266 log.debug('VCSMiddleware: detecting vcs type.')
263 267 handler = detect_vcs_request(environ, self.check_middlewares)
264 268 if handler:
265 269 app = handler(self.config, self.registry)
266 270
267 271 return app
268 272
269 273 def __call__(self, environ, start_response):
270 274 # check if we handle one of interesting protocols, optionally extract
271 275 # specific vcsSettings and allow changes of how things are wrapped
272 276 vcs_handler = self._get_handler_app(environ)
273 277 if vcs_handler:
274 278 # translate the _REPO_ID into real repo NAME for usage
275 279 # in middleware
276 280
277 281 path_info = get_path_info(environ)
278 282 environ['PATH_INFO'] = vcs_handler._get_by_id(path_info)
279 283
280 284 # Set acl, url and vcs repo names.
281 285 vcs_handler.set_repo_names(environ)
282 286
283 287 # register repo config back to the handler
284 288 vcs_conf = self.vcs_config(vcs_handler.acl_repo_name)
285 289 # maybe damaged/non existent settings. We still want to
286 290 # pass that point to validate on is_valid_and_existing_repo
287 291 # and return proper HTTP Code back to client
288 292 if vcs_conf:
289 293 vcs_handler.repo_vcs_config = vcs_conf
290 294
291 295 # check for type, presence in database and on filesystem
292 296 if not vcs_handler.is_valid_and_existing_repo(
293 297 vcs_handler.acl_repo_name,
294 298 vcs_handler.base_path,
295 299 vcs_handler.SCM):
296 300 return HTTPNotFound()(environ, start_response)
297 301
298 302 environ['REPO_NAME'] = vcs_handler.url_repo_name
299 303
300 304 # Wrap handler in middlewares if they are enabled.
301 305 vcs_handler = self.wrap_in_gzip_if_enabled(
302 306 vcs_handler, self.config)
303 307 vcs_handler, _ = wrap_in_appenlight_if_enabled(
304 308 vcs_handler, self.config, self.appenlight_client)
305 309
306 310 return vcs_handler(environ, start_response)
307 311
308 312 return self.application(environ, start_response)
General Comments 0
You need to be logged in to leave comments. Login now