##// END OF EJS Templates
logging: added http proto logging.
marcink -
r736:e2a18211 default
parent child Browse files
Show More
@@ -1,611 +1,611 b''
1 1 # RhodeCode VCSServer provides access to different vcs backends via network.
2 2 # Copyright (C) 2014-2019 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 200 def not_found(request):
201 201 return {'status': '404 NOT FOUND'}
202 202
203 203
204 204 class VCSViewPredicate(object):
205 205 def __init__(self, val, config):
206 206 self.remotes = val
207 207
208 208 def text(self):
209 209 return 'vcs view method = %s' % (self.remotes.keys(),)
210 210
211 211 phash = text
212 212
213 213 def __call__(self, context, request):
214 214 """
215 215 View predicate that returns true if given backend is supported by
216 216 defined remotes.
217 217 """
218 218 backend = request.matchdict.get('backend')
219 219 return backend in self.remotes
220 220
221 221
222 222 class HTTPApplication(object):
223 223 ALLOWED_EXCEPTIONS = ('KeyError', 'URLError')
224 224
225 225 remote_wsgi = remote_wsgi
226 226 _use_echo_app = False
227 227
228 228 def __init__(self, settings=None, global_config=None):
229 229 self._sanitize_settings_and_apply_defaults(settings)
230 230
231 231 self.config = Configurator(settings=settings)
232 232 self.global_config = global_config
233 233 self.config.include('vcsserver.lib.rc_cache')
234 234
235 235 locale = settings.get('locale', '') or 'en_US.UTF-8'
236 236 vcs = VCS(locale=locale, cache_config=settings)
237 237 self._remotes = {
238 238 'hg': vcs._hg_remote,
239 239 'git': vcs._git_remote,
240 240 'svn': vcs._svn_remote,
241 241 'server': vcs._vcsserver,
242 242 }
243 243 if settings.get('dev.use_echo_app', 'false').lower() == 'true':
244 244 self._use_echo_app = True
245 245 log.warning("Using EchoApp for VCS operations.")
246 246 self.remote_wsgi = remote_wsgi_stub
247 247
248 248 self._configure_settings(global_config, settings)
249 249 self._configure()
250 250
251 251 def _configure_settings(self, global_config, app_settings):
252 252 """
253 253 Configure the settings module.
254 254 """
255 255 settings_merged = global_config.copy()
256 256 settings_merged.update(app_settings)
257 257
258 258 git_path = app_settings.get('git_path', None)
259 259 if git_path:
260 260 settings.GIT_EXECUTABLE = git_path
261 261 binary_dir = app_settings.get('core.binary_dir', None)
262 262 if binary_dir:
263 263 settings.BINARY_DIR = binary_dir
264 264
265 265 # Store the settings to make them available to other modules.
266 266 vcsserver.PYRAMID_SETTINGS = settings_merged
267 267 vcsserver.CONFIG = settings_merged
268 268
269 269 def _sanitize_settings_and_apply_defaults(self, settings):
270 270 temp_store = tempfile.gettempdir()
271 271 default_cache_dir = os.path.join(temp_store, 'rc_cache')
272 272
273 273 # save default, cache dir, and use it for all backends later.
274 274 default_cache_dir = _string_setting(
275 275 settings,
276 276 'cache_dir',
277 277 default_cache_dir, lower=False, default_when_empty=True)
278 278
279 279 # ensure we have our dir created
280 280 if not os.path.isdir(default_cache_dir):
281 281 os.makedirs(default_cache_dir, mode=0o755)
282 282
283 283 # exception store cache
284 284 _string_setting(
285 285 settings,
286 286 'exception_tracker.store_path',
287 287 temp_store, lower=False, default_when_empty=True)
288 288
289 289 # repo_object cache
290 290 _string_setting(
291 291 settings,
292 292 'rc_cache.repo_object.backend',
293 293 'dogpile.cache.rc.memory_lru')
294 294 _int_setting(
295 295 settings,
296 296 'rc_cache.repo_object.expiration_time',
297 297 300)
298 298 _int_setting(
299 299 settings,
300 300 'rc_cache.repo_object.max_size',
301 301 1024)
302 302
303 303 def _configure(self):
304 304 self.config.add_renderer(name='msgpack', factory=self._msgpack_renderer_factory)
305 305
306 306 self.config.add_route('service', '/_service')
307 307 self.config.add_route('status', '/status')
308 308 self.config.add_route('hg_proxy', '/proxy/hg')
309 309 self.config.add_route('git_proxy', '/proxy/git')
310 310 self.config.add_route('vcs', '/{backend}')
311 311 self.config.add_route('stream_git', '/stream/git/*repo_name')
312 312 self.config.add_route('stream_hg', '/stream/hg/*repo_name')
313 313
314 314 self.config.add_view(self.status_view, route_name='status', renderer='json')
315 315 self.config.add_view(self.service_view, route_name='service', renderer='msgpack')
316 316
317 317 self.config.add_view(self.hg_proxy(), route_name='hg_proxy')
318 318 self.config.add_view(self.git_proxy(), route_name='git_proxy')
319 319 self.config.add_view(self.vcs_view, route_name='vcs', renderer='msgpack',
320 320 vcs_view=self._remotes)
321 321
322 322 self.config.add_view(self.hg_stream(), route_name='stream_hg')
323 323 self.config.add_view(self.git_stream(), route_name='stream_git')
324 324
325 325 self.config.add_view_predicate('vcs_view', VCSViewPredicate)
326 326
327 327 self.config.add_notfound_view(not_found, renderer='json')
328 328
329 329 self.config.add_view(self.handle_vcs_exception, context=Exception)
330 330
331 331 self.config.add_tween(
332 332 'vcsserver.tweens.RequestWrapperTween',
333 333 )
334 334
335 335 def wsgi_app(self):
336 336 return self.config.make_wsgi_app()
337 337
338 338 def vcs_view(self, request):
339 339 remote = self._remotes[request.matchdict['backend']]
340 340 payload = msgpack.unpackb(request.body, use_list=True)
341 341 method = payload.get('method')
342 342 params = payload.get('params')
343 343 wire = params.get('wire')
344 344 args = params.get('args')
345 345 kwargs = params.get('kwargs')
346 346 context_uid = None
347 347
348 348 if wire:
349 349 try:
350 350 wire['context'] = context_uid = uuid.UUID(wire['context'])
351 351 except KeyError:
352 352 pass
353 353 args.insert(0, wire)
354 354
355 log.debug('method called:%s with kwargs:%s context_uid: %s',
356 method, kwargs, context_uid)
355 log.debug('method called:%s with args:%s kwargs:%s context_uid: %s',
356 method, args[1:], kwargs, context_uid)
357 357
358 358 try:
359 359 resp = getattr(remote, method)(*args, **kwargs)
360 360 except Exception as e:
361 361 exc_info = list(sys.exc_info())
362 362 exc_type, exc_value, exc_traceback = exc_info
363 363
364 364 org_exc = getattr(e, '_org_exc', None)
365 365 org_exc_name = None
366 366 org_exc_tb = ''
367 367 if org_exc:
368 368 org_exc_name = org_exc.__class__.__name__
369 369 org_exc_tb = getattr(e, '_org_exc_tb', '')
370 370 # replace our "faked" exception with our org
371 371 exc_info[0] = org_exc.__class__
372 372 exc_info[1] = org_exc
373 373
374 374 store_exception(id(exc_info), exc_info)
375 375
376 376 tb_info = ''.join(
377 377 traceback.format_exception(exc_type, exc_value, exc_traceback))
378 378
379 379 type_ = e.__class__.__name__
380 380 if type_ not in self.ALLOWED_EXCEPTIONS:
381 381 type_ = None
382 382
383 383 resp = {
384 384 'id': payload.get('id'),
385 385 'error': {
386 386 'message': e.message,
387 387 'traceback': tb_info,
388 388 'org_exc': org_exc_name,
389 389 'org_exc_tb': org_exc_tb,
390 390 'type': type_
391 391 }
392 392 }
393 393 try:
394 394 resp['error']['_vcs_kind'] = getattr(e, '_vcs_kind', None)
395 395 except AttributeError:
396 396 pass
397 397 else:
398 398 resp = {
399 399 'id': payload.get('id'),
400 400 'result': resp
401 401 }
402 402
403 403 return resp
404 404
405 405 def status_view(self, request):
406 406 import vcsserver
407 407 return {'status': 'OK', 'vcsserver_version': vcsserver.__version__,
408 408 'pid': os.getpid()}
409 409
410 410 def service_view(self, request):
411 411 import vcsserver
412 412
413 413 payload = msgpack.unpackb(request.body, use_list=True)
414 414
415 415 try:
416 416 path = self.global_config['__file__']
417 417 config = configparser.ConfigParser()
418 418 config.read(path)
419 419 parsed_ini = config
420 420 if parsed_ini.has_section('server:main'):
421 421 parsed_ini = dict(parsed_ini.items('server:main'))
422 422 except Exception:
423 423 log.exception('Failed to read .ini file for display')
424 424 parsed_ini = {}
425 425
426 426 resp = {
427 427 'id': payload.get('id'),
428 428 'result': dict(
429 429 version=vcsserver.__version__,
430 430 config=parsed_ini,
431 431 payload=payload,
432 432 )
433 433 }
434 434 return resp
435 435
436 436 def _msgpack_renderer_factory(self, info):
437 437 def _render(value, system):
438 438 value = msgpack.packb(value)
439 439 request = system.get('request')
440 440 if request is not None:
441 441 response = request.response
442 442 ct = response.content_type
443 443 if ct == response.default_content_type:
444 444 response.content_type = 'application/x-msgpack'
445 445 return value
446 446 return _render
447 447
448 448 def set_env_from_config(self, environ, config):
449 449 dict_conf = {}
450 450 try:
451 451 for elem in config:
452 452 if elem[0] == 'rhodecode':
453 453 dict_conf = json.loads(elem[2])
454 454 break
455 455 except Exception:
456 456 log.exception('Failed to fetch SCM CONFIG')
457 457 return
458 458
459 459 username = dict_conf.get('username')
460 460 if username:
461 461 environ['REMOTE_USER'] = username
462 462 # mercurial specific, some extension api rely on this
463 463 environ['HGUSER'] = username
464 464
465 465 ip = dict_conf.get('ip')
466 466 if ip:
467 467 environ['REMOTE_HOST'] = ip
468 468
469 469 if _is_request_chunked(environ):
470 470 # set the compatibility flag for webob
471 471 environ['wsgi.input_terminated'] = True
472 472
473 473 def hg_proxy(self):
474 474 @wsgiapp
475 475 def _hg_proxy(environ, start_response):
476 476 app = WsgiProxy(self.remote_wsgi.HgRemoteWsgi())
477 477 return app(environ, start_response)
478 478 return _hg_proxy
479 479
480 480 def git_proxy(self):
481 481 @wsgiapp
482 482 def _git_proxy(environ, start_response):
483 483 app = WsgiProxy(self.remote_wsgi.GitRemoteWsgi())
484 484 return app(environ, start_response)
485 485 return _git_proxy
486 486
487 487 def hg_stream(self):
488 488 if self._use_echo_app:
489 489 @wsgiapp
490 490 def _hg_stream(environ, start_response):
491 491 app = EchoApp('fake_path', 'fake_name', None)
492 492 return app(environ, start_response)
493 493 return _hg_stream
494 494 else:
495 495 @wsgiapp
496 496 def _hg_stream(environ, start_response):
497 497 log.debug('http-app: handling hg stream')
498 498 repo_path = environ['HTTP_X_RC_REPO_PATH']
499 499 repo_name = environ['HTTP_X_RC_REPO_NAME']
500 500 packed_config = base64.b64decode(
501 501 environ['HTTP_X_RC_REPO_CONFIG'])
502 502 config = msgpack.unpackb(packed_config)
503 503 app = scm_app.create_hg_wsgi_app(
504 504 repo_path, repo_name, config)
505 505
506 506 # Consistent path information for hgweb
507 507 environ['PATH_INFO'] = environ['HTTP_X_RC_PATH_INFO']
508 508 environ['REPO_NAME'] = repo_name
509 509 self.set_env_from_config(environ, config)
510 510
511 511 log.debug('http-app: starting app handler '
512 512 'with %s and process request', app)
513 513 return app(environ, ResponseFilter(start_response))
514 514 return _hg_stream
515 515
516 516 def git_stream(self):
517 517 if self._use_echo_app:
518 518 @wsgiapp
519 519 def _git_stream(environ, start_response):
520 520 app = EchoApp('fake_path', 'fake_name', None)
521 521 return app(environ, start_response)
522 522 return _git_stream
523 523 else:
524 524 @wsgiapp
525 525 def _git_stream(environ, start_response):
526 526 log.debug('http-app: handling git stream')
527 527 repo_path = environ['HTTP_X_RC_REPO_PATH']
528 528 repo_name = environ['HTTP_X_RC_REPO_NAME']
529 529 packed_config = base64.b64decode(
530 530 environ['HTTP_X_RC_REPO_CONFIG'])
531 531 config = msgpack.unpackb(packed_config)
532 532
533 533 environ['PATH_INFO'] = environ['HTTP_X_RC_PATH_INFO']
534 534 self.set_env_from_config(environ, config)
535 535
536 536 content_type = environ.get('CONTENT_TYPE', '')
537 537
538 538 path = environ['PATH_INFO']
539 539 is_lfs_request = GIT_LFS_CONTENT_TYPE in content_type
540 540 log.debug(
541 541 'LFS: Detecting if request `%s` is LFS server path based '
542 542 'on content type:`%s`, is_lfs:%s',
543 543 path, content_type, is_lfs_request)
544 544
545 545 if not is_lfs_request:
546 546 # fallback detection by path
547 547 if GIT_LFS_PROTO_PAT.match(path):
548 548 is_lfs_request = True
549 549 log.debug(
550 550 'LFS: fallback detection by path of: `%s`, is_lfs:%s',
551 551 path, is_lfs_request)
552 552
553 553 if is_lfs_request:
554 554 app = scm_app.create_git_lfs_wsgi_app(
555 555 repo_path, repo_name, config)
556 556 else:
557 557 app = scm_app.create_git_wsgi_app(
558 558 repo_path, repo_name, config)
559 559
560 560 log.debug('http-app: starting app handler '
561 561 'with %s and process request', app)
562 562
563 563 return app(environ, start_response)
564 564
565 565 return _git_stream
566 566
567 567 def handle_vcs_exception(self, exception, request):
568 568 _vcs_kind = getattr(exception, '_vcs_kind', '')
569 569 if _vcs_kind == 'repo_locked':
570 570 # Get custom repo-locked status code if present.
571 571 status_code = request.headers.get('X-RC-Locked-Status-Code')
572 572 return HTTPRepoLocked(
573 573 title=exception.message, status_code=status_code)
574 574
575 575 elif _vcs_kind == 'repo_branch_protected':
576 576 # Get custom repo-branch-protected status code if present.
577 577 return HTTPRepoBranchProtected(title=exception.message)
578 578
579 579 exc_info = request.exc_info
580 580 store_exception(id(exc_info), exc_info)
581 581
582 582 traceback_info = 'unavailable'
583 583 if request.exc_info:
584 584 exc_type, exc_value, exc_tb = request.exc_info
585 585 traceback_info = ''.join(traceback.format_exception(exc_type, exc_value, exc_tb))
586 586
587 587 log.error(
588 588 'error occurred handling this request for path: %s, \n tb: %s',
589 589 request.path, traceback_info)
590 590 raise exception
591 591
592 592
593 593 class ResponseFilter(object):
594 594
595 595 def __init__(self, start_response):
596 596 self._start_response = start_response
597 597
598 598 def __call__(self, status, response_headers, exc_info=None):
599 599 headers = tuple(
600 600 (h, v) for h, v in response_headers
601 601 if not wsgiref.util.is_hop_by_hop(h))
602 602 return self._start_response(status, headers, exc_info)
603 603
604 604
605 605 def main(global_config, **settings):
606 606 if MercurialFactory:
607 607 hgpatches.patch_largefiles_capabilities()
608 608 hgpatches.patch_subrepo_type_mapping()
609 609
610 610 app = HTTPApplication(settings=settings, global_config=global_config)
611 611 return app.wsgi_app()
General Comments 0
You need to be logged in to leave comments. Login now