##// END OF EJS Templates
pyramid: don't use deprecated custom_predicates in view config
marcink -
r583:d119c397 default
parent child Browse files
Show More
@@ -1,598 +1,607 b''
1 1 # RhodeCode VCSServer provides access to different vcs backends via network.
2 2 # Copyright (C) 2014-2018 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 General Public License as published by
6 6 # the Free Software Foundation; either version 3 of the License, or
7 7 # (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software Foundation,
16 16 # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
17 17
18 18 import os
19 19 import sys
20 20 import base64
21 21 import locale
22 22 import logging
23 23 import uuid
24 24 import wsgiref.util
25 25 import traceback
26 26 import tempfile
27 27 from itertools import chain
28 28
29 29 import simplejson as json
30 30 import msgpack
31 31 from pyramid.config import Configurator
32 32 from pyramid.settings import asbool, aslist
33 33 from pyramid.wsgi import wsgiapp
34 34 from pyramid.compat import configparser
35 35
36 36
37 37 log = logging.getLogger(__name__)
38 38
39 39 # due to Mercurial/glibc2.27 problems we need to detect if locale settings are
40 40 # causing problems and "fix" it in case they do and fallback to LC_ALL = C
41 41
42 42 try:
43 43 locale.setlocale(locale.LC_ALL, '')
44 44 except locale.Error as e:
45 45 log.error(
46 46 'LOCALE ERROR: failed to set LC_ALL, fallback to LC_ALL=C, org error: %s', e)
47 47 os.environ['LC_ALL'] = 'C'
48 48
49 49 import vcsserver
50 50 from vcsserver import remote_wsgi, scm_app, settings, hgpatches
51 51 from vcsserver.git_lfs.app import GIT_LFS_CONTENT_TYPE, GIT_LFS_PROTO_PAT
52 52 from vcsserver.echo_stub import remote_wsgi as remote_wsgi_stub
53 53 from vcsserver.echo_stub.echo_app import EchoApp
54 54 from vcsserver.exceptions import HTTPRepoLocked, HTTPRepoBranchProtected
55 55 from vcsserver.lib.exc_tracking import store_exception
56 56 from vcsserver.server import VcsServer
57 57
58 58 try:
59 59 from vcsserver.git import GitFactory, GitRemote
60 60 except ImportError:
61 61 GitFactory = None
62 62 GitRemote = None
63 63
64 64 try:
65 65 from vcsserver.hg import MercurialFactory, HgRemote
66 66 except ImportError:
67 67 MercurialFactory = None
68 68 HgRemote = None
69 69
70 70 try:
71 71 from vcsserver.svn import SubversionFactory, SvnRemote
72 72 except ImportError:
73 73 SubversionFactory = None
74 74 SvnRemote = None
75 75
76 76
77 77 def _is_request_chunked(environ):
78 78 stream = environ.get('HTTP_TRANSFER_ENCODING', '') == 'chunked'
79 79 return stream
80 80
81 81
82 82 def _int_setting(settings, name, default):
83 83 settings[name] = int(settings.get(name, default))
84 84 return settings[name]
85 85
86 86
87 87 def _bool_setting(settings, name, default):
88 88 input_val = settings.get(name, default)
89 89 if isinstance(input_val, unicode):
90 90 input_val = input_val.encode('utf8')
91 91 settings[name] = asbool(input_val)
92 92 return settings[name]
93 93
94 94
95 95 def _list_setting(settings, name, default):
96 96 raw_value = settings.get(name, default)
97 97
98 98 # Otherwise we assume it uses pyramids space/newline separation.
99 99 settings[name] = aslist(raw_value)
100 100 return settings[name]
101 101
102 102
103 103 def _string_setting(settings, name, default, lower=True, default_when_empty=False):
104 104 value = settings.get(name, default)
105 105
106 106 if default_when_empty and not value:
107 107 # use default value when value is empty
108 108 value = default
109 109
110 110 if lower:
111 111 value = value.lower()
112 112 settings[name] = value
113 113 return settings[name]
114 114
115 115
116 116 class VCS(object):
117 117 def __init__(self, locale=None, cache_config=None):
118 118 self.locale = locale
119 119 self.cache_config = cache_config
120 120 self._configure_locale()
121 121
122 122 if GitFactory and GitRemote:
123 123 git_factory = GitFactory()
124 124 self._git_remote = GitRemote(git_factory)
125 125 else:
126 126 log.info("Git client import failed")
127 127
128 128 if MercurialFactory and HgRemote:
129 129 hg_factory = MercurialFactory()
130 130 self._hg_remote = HgRemote(hg_factory)
131 131 else:
132 132 log.info("Mercurial client import failed")
133 133
134 134 if SubversionFactory and SvnRemote:
135 135 svn_factory = SubversionFactory()
136 136
137 137 # hg factory is used for svn url validation
138 138 hg_factory = MercurialFactory()
139 139 self._svn_remote = SvnRemote(svn_factory, hg_factory=hg_factory)
140 140 else:
141 141 log.info("Subversion client import failed")
142 142
143 143 self._vcsserver = VcsServer()
144 144
145 145 def _configure_locale(self):
146 146 if self.locale:
147 147 log.info('Settings locale: `LC_ALL` to %s', self.locale)
148 148 else:
149 149 log.info(
150 150 'Configuring locale subsystem based on environment variables')
151 151 try:
152 152 # If self.locale is the empty string, then the locale
153 153 # module will use the environment variables. See the
154 154 # documentation of the package `locale`.
155 155 locale.setlocale(locale.LC_ALL, self.locale)
156 156
157 157 language_code, encoding = locale.getlocale()
158 158 log.info(
159 159 'Locale set to language code "%s" with encoding "%s".',
160 160 language_code, encoding)
161 161 except locale.Error:
162 162 log.exception(
163 163 'Cannot set locale, not configuring the locale system')
164 164
165 165
166 166 class WsgiProxy(object):
167 167 def __init__(self, wsgi):
168 168 self.wsgi = wsgi
169 169
170 170 def __call__(self, environ, start_response):
171 171 input_data = environ['wsgi.input'].read()
172 172 input_data = msgpack.unpackb(input_data)
173 173
174 174 error = None
175 175 try:
176 176 data, status, headers = self.wsgi.handle(
177 177 input_data['environment'], input_data['input_data'],
178 178 *input_data['args'], **input_data['kwargs'])
179 179 except Exception as e:
180 180 data, status, headers = [], None, None
181 181 error = {
182 182 'message': str(e),
183 183 '_vcs_kind': getattr(e, '_vcs_kind', None)
184 184 }
185 185
186 186 start_response(200, {})
187 187 return self._iterator(error, status, headers, data)
188 188
189 189 def _iterator(self, error, status, headers, data):
190 190 initial_data = [
191 191 error,
192 192 status,
193 193 headers,
194 194 ]
195 195
196 196 for d in chain(initial_data, data):
197 197 yield msgpack.packb(d)
198 198
199 199
200 def not_found(request):
201 return {'status': '404 NOT FOUND'}
202
203
204 class VCSViewPredicate(object):
205 def __init__(self, val, config):
206 self.remotes = val
207
208 def text(self):
209 return 'vcs view method = %s' % (self.remotes.keys(),)
210
211 phash = text
212
213 def __call__(self, context, request):
214 """
215 View predicate that returns true if given backend is supported by
216 defined remotes.
217 """
218 backend = request.matchdict.get('backend')
219 return backend in self.remotes
220
221
200 222 class HTTPApplication(object):
201 223 ALLOWED_EXCEPTIONS = ('KeyError', 'URLError')
202 224
203 225 remote_wsgi = remote_wsgi
204 226 _use_echo_app = False
205 227
206 228 def __init__(self, settings=None, global_config=None):
207 229 self._sanitize_settings_and_apply_defaults(settings)
208 230
209 231 self.config = Configurator(settings=settings)
210 232 self.global_config = global_config
211 233 self.config.include('vcsserver.lib.rc_cache')
212 234
213 235 locale = settings.get('locale', '') or 'en_US.UTF-8'
214 236 vcs = VCS(locale=locale, cache_config=settings)
215 237 self._remotes = {
216 238 'hg': vcs._hg_remote,
217 239 'git': vcs._git_remote,
218 240 'svn': vcs._svn_remote,
219 241 'server': vcs._vcsserver,
220 242 }
221 243 if settings.get('dev.use_echo_app', 'false').lower() == 'true':
222 244 self._use_echo_app = True
223 245 log.warning("Using EchoApp for VCS operations.")
224 246 self.remote_wsgi = remote_wsgi_stub
225 247
226 248 self._configure_settings(global_config, settings)
227 249 self._configure()
228 250
229 251 def _configure_settings(self, global_config, app_settings):
230 252 """
231 253 Configure the settings module.
232 254 """
233 255 settings_merged = global_config.copy()
234 256 settings_merged.update(app_settings)
235 257
236 258 git_path = app_settings.get('git_path', None)
237 259 if git_path:
238 260 settings.GIT_EXECUTABLE = git_path
239 261 binary_dir = app_settings.get('core.binary_dir', None)
240 262 if binary_dir:
241 263 settings.BINARY_DIR = binary_dir
242 264
243 265 # Store the settings to make them available to other modules.
244 266 vcsserver.PYRAMID_SETTINGS = settings_merged
245 267 vcsserver.CONFIG = settings_merged
246 268
247 269 def _sanitize_settings_and_apply_defaults(self, settings):
248 270 temp_store = tempfile.gettempdir()
249 271 default_cache_dir = os.path.join(temp_store, 'rc_cache')
250 272
251 273 # save default, cache dir, and use it for all backends later.
252 274 default_cache_dir = _string_setting(
253 275 settings,
254 276 'cache_dir',
255 277 default_cache_dir, lower=False, default_when_empty=True)
256 278
257 279 # ensure we have our dir created
258 280 if not os.path.isdir(default_cache_dir):
259 281 os.makedirs(default_cache_dir, mode=0755)
260 282
261 283 # exception store cache
262 284 _string_setting(
263 285 settings,
264 286 'exception_tracker.store_path',
265 287 temp_store, lower=False, default_when_empty=True)
266 288
267 289 # repo_object cache
268 290 _string_setting(
269 291 settings,
270 292 'rc_cache.repo_object.backend',
271 293 'dogpile.cache.rc.memory_lru')
272 294 _int_setting(
273 295 settings,
274 296 'rc_cache.repo_object.expiration_time',
275 297 300)
276 298 _int_setting(
277 299 settings,
278 300 'rc_cache.repo_object.max_size',
279 301 1024)
280 302
281 303 def _configure(self):
282 self.config.add_renderer(
283 name='msgpack',
284 factory=self._msgpack_renderer_factory)
304 self.config.add_renderer(name='msgpack', factory=self._msgpack_renderer_factory)
285 305
286 306 self.config.add_route('service', '/_service')
287 307 self.config.add_route('status', '/status')
288 308 self.config.add_route('hg_proxy', '/proxy/hg')
289 309 self.config.add_route('git_proxy', '/proxy/git')
290 310 self.config.add_route('vcs', '/{backend}')
291 311 self.config.add_route('stream_git', '/stream/git/*repo_name')
292 312 self.config.add_route('stream_hg', '/stream/hg/*repo_name')
293 313
294 self.config.add_view(
295 self.status_view, route_name='status', renderer='json')
296 self.config.add_view(
297 self.service_view, route_name='service', renderer='msgpack')
314 self.config.add_view(self.status_view, route_name='status', renderer='json')
315 self.config.add_view(self.service_view, route_name='service', renderer='msgpack')
298 316
299 317 self.config.add_view(self.hg_proxy(), route_name='hg_proxy')
300 318 self.config.add_view(self.git_proxy(), route_name='git_proxy')
301 self.config.add_view(
302 self.vcs_view, route_name='vcs', renderer='msgpack',
303 custom_predicates=[self.is_vcs_view])
319 self.config.add_view(self.vcs_view, route_name='vcs', renderer='msgpack',
320 vcs_view=self._remotes)
304 321
305 322 self.config.add_view(self.hg_stream(), route_name='stream_hg')
306 323 self.config.add_view(self.git_stream(), route_name='stream_git')
307 324
308 def notfound(request):
309 return {'status': '404 NOT FOUND'}
310 self.config.add_notfound_view(notfound, renderer='json')
325 self.config.add_view_predicate('vcs_view', VCSViewPredicate)
326
327 self.config.add_notfound_view(not_found, renderer='json')
311 328
312 329 self.config.add_view(self.handle_vcs_exception, context=Exception)
313 330
314 331 self.config.add_tween(
315 332 'vcsserver.tweens.RequestWrapperTween',
316 333 )
317 334
318 335 def wsgi_app(self):
319 336 return self.config.make_wsgi_app()
320 337
321 338 def vcs_view(self, request):
322 339 remote = self._remotes[request.matchdict['backend']]
323 340 payload = msgpack.unpackb(request.body, use_list=True)
324 341 method = payload.get('method')
325 342 params = payload.get('params')
326 343 wire = params.get('wire')
327 344 args = params.get('args')
328 345 kwargs = params.get('kwargs')
329 346 context_uid = None
330 347
331 348 if wire:
332 349 try:
333 350 wire['context'] = context_uid = uuid.UUID(wire['context'])
334 351 except KeyError:
335 352 pass
336 353 args.insert(0, wire)
337 354
338 355 log.debug('method called:%s with kwargs:%s context_uid: %s',
339 356 method, kwargs, context_uid)
340 357 try:
341 358 resp = getattr(remote, method)(*args, **kwargs)
342 359 except Exception as e:
343 360 exc_info = list(sys.exc_info())
344 361 exc_type, exc_value, exc_traceback = exc_info
345 362
346 363 org_exc = getattr(e, '_org_exc', None)
347 364 org_exc_name = None
348 365 if org_exc:
349 366 org_exc_name = org_exc.__class__.__name__
350 367 # replace our "faked" exception with our org
351 368 exc_info[0] = org_exc.__class__
352 369 exc_info[1] = org_exc
353 370
354 371 store_exception(id(exc_info), exc_info)
355 372
356 373 tb_info = ''.join(
357 374 traceback.format_exception(exc_type, exc_value, exc_traceback))
358 375
359 376 type_ = e.__class__.__name__
360 377 if type_ not in self.ALLOWED_EXCEPTIONS:
361 378 type_ = None
362 379
363 380 resp = {
364 381 'id': payload.get('id'),
365 382 'error': {
366 383 'message': e.message,
367 384 'traceback': tb_info,
368 385 'org_exc': org_exc_name,
369 386 'type': type_
370 387 }
371 388 }
372 389 try:
373 390 resp['error']['_vcs_kind'] = getattr(e, '_vcs_kind', None)
374 391 except AttributeError:
375 392 pass
376 393 else:
377 394 resp = {
378 395 'id': payload.get('id'),
379 396 'result': resp
380 397 }
381 398
382 399 return resp
383 400
384 401 def status_view(self, request):
385 402 import vcsserver
386 403 return {'status': 'OK', 'vcsserver_version': vcsserver.__version__,
387 404 'pid': os.getpid()}
388 405
389 406 def service_view(self, request):
390 407 import vcsserver
391 408
392 409 payload = msgpack.unpackb(request.body, use_list=True)
393 410
394 411 try:
395 412 path = self.global_config['__file__']
396 413 config = configparser.ConfigParser()
397 414 config.read(path)
398 415 parsed_ini = config
399 416 if parsed_ini.has_section('server:main'):
400 417 parsed_ini = dict(parsed_ini.items('server:main'))
401 418 except Exception:
402 419 log.exception('Failed to read .ini file for display')
403 420 parsed_ini = {}
404 421
405 422 resp = {
406 423 'id': payload.get('id'),
407 424 'result': dict(
408 425 version=vcsserver.__version__,
409 426 config=parsed_ini,
410 427 payload=payload,
411 428 )
412 429 }
413 430 return resp
414 431
415 432 def _msgpack_renderer_factory(self, info):
416 433 def _render(value, system):
417 434 value = msgpack.packb(value)
418 435 request = system.get('request')
419 436 if request is not None:
420 437 response = request.response
421 438 ct = response.content_type
422 439 if ct == response.default_content_type:
423 440 response.content_type = 'application/x-msgpack'
424 441 return value
425 442 return _render
426 443
427 444 def set_env_from_config(self, environ, config):
428 445 dict_conf = {}
429 446 try:
430 447 for elem in config:
431 448 if elem[0] == 'rhodecode':
432 449 dict_conf = json.loads(elem[2])
433 450 break
434 451 except Exception:
435 452 log.exception('Failed to fetch SCM CONFIG')
436 453 return
437 454
438 455 username = dict_conf.get('username')
439 456 if username:
440 457 environ['REMOTE_USER'] = username
441 458 # mercurial specific, some extension api rely on this
442 459 environ['HGUSER'] = username
443 460
444 461 ip = dict_conf.get('ip')
445 462 if ip:
446 463 environ['REMOTE_HOST'] = ip
447 464
448 465 if _is_request_chunked(environ):
449 466 # set the compatibility flag for webob
450 467 environ['wsgi.input_terminated'] = True
451 468
452 469 def hg_proxy(self):
453 470 @wsgiapp
454 471 def _hg_proxy(environ, start_response):
455 472 app = WsgiProxy(self.remote_wsgi.HgRemoteWsgi())
456 473 return app(environ, start_response)
457 474 return _hg_proxy
458 475
459 476 def git_proxy(self):
460 477 @wsgiapp
461 478 def _git_proxy(environ, start_response):
462 479 app = WsgiProxy(self.remote_wsgi.GitRemoteWsgi())
463 480 return app(environ, start_response)
464 481 return _git_proxy
465 482
466 483 def hg_stream(self):
467 484 if self._use_echo_app:
468 485 @wsgiapp
469 486 def _hg_stream(environ, start_response):
470 487 app = EchoApp('fake_path', 'fake_name', None)
471 488 return app(environ, start_response)
472 489 return _hg_stream
473 490 else:
474 491 @wsgiapp
475 492 def _hg_stream(environ, start_response):
476 493 log.debug('http-app: handling hg stream')
477 494 repo_path = environ['HTTP_X_RC_REPO_PATH']
478 495 repo_name = environ['HTTP_X_RC_REPO_NAME']
479 496 packed_config = base64.b64decode(
480 497 environ['HTTP_X_RC_REPO_CONFIG'])
481 498 config = msgpack.unpackb(packed_config)
482 499 app = scm_app.create_hg_wsgi_app(
483 500 repo_path, repo_name, config)
484 501
485 502 # Consistent path information for hgweb
486 503 environ['PATH_INFO'] = environ['HTTP_X_RC_PATH_INFO']
487 504 environ['REPO_NAME'] = repo_name
488 505 self.set_env_from_config(environ, config)
489 506
490 507 log.debug('http-app: starting app handler '
491 508 'with %s and process request', app)
492 509 return app(environ, ResponseFilter(start_response))
493 510 return _hg_stream
494 511
495 512 def git_stream(self):
496 513 if self._use_echo_app:
497 514 @wsgiapp
498 515 def _git_stream(environ, start_response):
499 516 app = EchoApp('fake_path', 'fake_name', None)
500 517 return app(environ, start_response)
501 518 return _git_stream
502 519 else:
503 520 @wsgiapp
504 521 def _git_stream(environ, start_response):
505 522 log.debug('http-app: handling git stream')
506 523 repo_path = environ['HTTP_X_RC_REPO_PATH']
507 524 repo_name = environ['HTTP_X_RC_REPO_NAME']
508 525 packed_config = base64.b64decode(
509 526 environ['HTTP_X_RC_REPO_CONFIG'])
510 527 config = msgpack.unpackb(packed_config)
511 528
512 529 environ['PATH_INFO'] = environ['HTTP_X_RC_PATH_INFO']
513 530 self.set_env_from_config(environ, config)
514 531
515 532 content_type = environ.get('CONTENT_TYPE', '')
516 533
517 534 path = environ['PATH_INFO']
518 535 is_lfs_request = GIT_LFS_CONTENT_TYPE in content_type
519 536 log.debug(
520 537 'LFS: Detecting if request `%s` is LFS server path based '
521 538 'on content type:`%s`, is_lfs:%s',
522 539 path, content_type, is_lfs_request)
523 540
524 541 if not is_lfs_request:
525 542 # fallback detection by path
526 543 if GIT_LFS_PROTO_PAT.match(path):
527 544 is_lfs_request = True
528 545 log.debug(
529 546 'LFS: fallback detection by path of: `%s`, is_lfs:%s',
530 547 path, is_lfs_request)
531 548
532 549 if is_lfs_request:
533 550 app = scm_app.create_git_lfs_wsgi_app(
534 551 repo_path, repo_name, config)
535 552 else:
536 553 app = scm_app.create_git_wsgi_app(
537 554 repo_path, repo_name, config)
538 555
539 556 log.debug('http-app: starting app handler '
540 557 'with %s and process request', app)
541 558
542 559 return app(environ, start_response)
543 560
544 561 return _git_stream
545 562
546 def is_vcs_view(self, context, request):
547 """
548 View predicate that returns true if given backend is supported by
549 defined remotes.
550 """
551 backend = request.matchdict.get('backend')
552 return backend in self._remotes
553
554 563 def handle_vcs_exception(self, exception, request):
555 564 _vcs_kind = getattr(exception, '_vcs_kind', '')
556 565 if _vcs_kind == 'repo_locked':
557 566 # Get custom repo-locked status code if present.
558 567 status_code = request.headers.get('X-RC-Locked-Status-Code')
559 568 return HTTPRepoLocked(
560 569 title=exception.message, status_code=status_code)
561 570
562 571 elif _vcs_kind == 'repo_branch_protected':
563 572 # Get custom repo-branch-protected status code if present.
564 573 return HTTPRepoBranchProtected(title=exception.message)
565 574
566 575 exc_info = request.exc_info
567 576 store_exception(id(exc_info), exc_info)
568 577
569 578 traceback_info = 'unavailable'
570 579 if request.exc_info:
571 580 exc_type, exc_value, exc_tb = request.exc_info
572 581 traceback_info = ''.join(traceback.format_exception(exc_type, exc_value, exc_tb))
573 582
574 583 log.error(
575 584 'error occurred handling this request for path: %s, \n tb: %s',
576 585 request.path, traceback_info)
577 586 raise exception
578 587
579 588
580 589 class ResponseFilter(object):
581 590
582 591 def __init__(self, start_response):
583 592 self._start_response = start_response
584 593
585 594 def __call__(self, status, response_headers, exc_info=None):
586 595 headers = tuple(
587 596 (h, v) for h, v in response_headers
588 597 if not wsgiref.util.is_hop_by_hop(h))
589 598 return self._start_response(status, headers, exc_info)
590 599
591 600
592 601 def main(global_config, **settings):
593 602 if MercurialFactory:
594 603 hgpatches.patch_largefiles_capabilities()
595 604 hgpatches.patch_subrepo_type_mapping()
596 605
597 606 app = HTTPApplication(settings=settings, global_config=global_config)
598 607 return app.wsgi_app()
General Comments 0
You need to be logged in to leave comments. Login now