##// END OF EJS Templates
project: switched completly to pyproject.toml
super-admin -
r1101:e8c454cf python3
parent child Browse files
Show More
@@ -1,24 +1,51 b''
1 [build-system]
2 requires = ["setuptools>=61.0.0", "wheel"]
3 build-backend = "setuptools.build_meta"
4
1 5 [project]
2 6 name = "rhodecode-vcsserver"
3 description = ""
7 description = "Version Control System Server for RhodeCode"
8 authors = [
9 {name = "RhodeCode GmbH", email = "support@rhodecode.com"},
10 ]
11
12 license = {text = "GPL V3"}
4 13 requires-python = ">=3.10"
5 version = "5.0.0"
14 dynamic = ["version", "readme"]
15 classifiers = [
16 'Development Status :: 6 - Mature',
17 'Intended Audience :: Developers',
18 'Operating System :: OS Independent',
19 'Topic :: Software Development :: Version Control',
20 'License :: OSI Approved :: GNU General Public License v3 or later (GPLv3+)',
21 'Programming Language :: Python :: 3.10',
22 ]
23
24 [project.entry-points."paste.app_factory"]
25 main = "vcsserver.http_main:main"
26
27 [tool.setuptools]
28 packages = ["vcsserver"]
29
30 [tool.setuptools.dynamic]
31 readme = {file = ["README.rst"], content-type = "text/rst"}
32 version = {file = "vcsserver/VERSION"}
6 33
7 34 [tool.ruff]
8 35 select = [
9 36 # Pyflakes
10 37 "F",
11 38 # Pycodestyle
12 39 "E",
13 40 "W",
14 41 # isort
15 42 "I001"
16 43 ]
17 44 ignore = [
18 45 "E501", # line too long, handled by black
19 46 ]
20 47 # Same as Black.
21 48 line-length = 120
22 49
23 50 [tool.ruff.isort]
24 51 known-first-party = ["vcsserver"]
@@ -1,28 +1,28 b''
1 1 # RhodeCode VCSServer provides access to different vcs backends via network.
2 2 # Copyright (C) 2014-2020 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 pkgutil
19 19
20 20
21 __version__ = pkgutil.get_data('vcsserver', 'VERSION').strip()
21 __version__ = pkgutil.get_data('vcsserver', 'VERSION').strip().decode()
22 22
23 23 # link to config for pyramid
24 24 CONFIG = {}
25 25
26 26 # Populated with the settings dictionary from application init in
27 27 #
28 28 PYRAMID_SETTINGS = {}
@@ -1,770 +1,770 b''
1 1 # RhodeCode VCSServer provides access to different vcs backends via network.
2 2 # Copyright (C) 2014-2020 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 io
19 19 import os
20 20 import sys
21 21 import base64
22 22 import locale
23 23 import logging
24 24 import uuid
25 25 import time
26 26 import wsgiref.util
27 27 import traceback
28 28 import tempfile
29 29 import psutil
30 30
31 31 from itertools import chain
32 32
33 33 import msgpack
34 34 import configparser
35 35
36 36 from pyramid.config import Configurator
37 37 from pyramid.wsgi import wsgiapp
38 38 from pyramid.response import Response
39 39
40 40 from vcsserver.base import BinaryEnvelope
41 41 from vcsserver.lib.rc_json import json
42 42 from vcsserver.config.settings_maker import SettingsMaker
43 43 from vcsserver.str_utils import safe_int, safe_bytes, safe_str
44 44 from vcsserver.lib.statsd_client import StatsdClient
45 45
46 46 log = logging.getLogger(__name__)
47 47
48 48 # due to Mercurial/glibc2.27 problems we need to detect if locale settings are
49 49 # causing problems and "fix" it in case they do and fallback to LC_ALL = C
50 50
51 51 try:
52 52 locale.setlocale(locale.LC_ALL, '')
53 53 except locale.Error as e:
54 54 log.error(
55 55 'LOCALE ERROR: failed to set LC_ALL, fallback to LC_ALL=C, org error: %s', e)
56 56 os.environ['LC_ALL'] = 'C'
57 57
58 58
59 59 import vcsserver
60 60 from vcsserver import remote_wsgi, scm_app, settings, hgpatches
61 61 from vcsserver.git_lfs.app import GIT_LFS_CONTENT_TYPE, GIT_LFS_PROTO_PAT
62 62 from vcsserver.echo_stub import remote_wsgi as remote_wsgi_stub
63 63 from vcsserver.echo_stub.echo_app import EchoApp
64 64 from vcsserver.exceptions import HTTPRepoLocked, HTTPRepoBranchProtected
65 65 from vcsserver.lib.exc_tracking import store_exception
66 66 from vcsserver.server import VcsServer
67 67
68 68 strict_vcs = True
69 69
70 70 git_import_err = None
71 71 try:
72 72 from vcsserver.remote.git import GitFactory, GitRemote
73 73 except ImportError as e:
74 74 GitFactory = None
75 75 GitRemote = None
76 76 git_import_err = e
77 77 if strict_vcs:
78 78 raise
79 79
80 80
81 81 hg_import_err = None
82 82 try:
83 83 from vcsserver.remote.hg import MercurialFactory, HgRemote
84 84 except ImportError as e:
85 85 MercurialFactory = None
86 86 HgRemote = None
87 87 hg_import_err = e
88 88 if strict_vcs:
89 89 raise
90 90
91 91
92 92 svn_import_err = None
93 93 try:
94 94 from vcsserver.remote.svn import SubversionFactory, SvnRemote
95 95 except ImportError as e:
96 96 SubversionFactory = None
97 97 SvnRemote = None
98 98 svn_import_err = e
99 99 if strict_vcs:
100 100 raise
101 101
102 102
103 103 def _is_request_chunked(environ):
104 104 stream = environ.get('HTTP_TRANSFER_ENCODING', '') == 'chunked'
105 105 return stream
106 106
107 107
108 108 def log_max_fd():
109 109 try:
110 110 maxfd = psutil.Process().rlimit(psutil.RLIMIT_NOFILE)[1]
111 111 log.info('Max file descriptors value: %s', maxfd)
112 112 except Exception:
113 113 pass
114 114
115 115
116 116 class VCS(object):
117 117 def __init__(self, locale_conf=None, cache_config=None):
118 118 self.locale = locale_conf
119 119 self.cache_config = cache_config
120 120 self._configure_locale()
121 121
122 122 log_max_fd()
123 123
124 124 if GitFactory and GitRemote:
125 125 git_factory = GitFactory()
126 126 self._git_remote = GitRemote(git_factory)
127 127 else:
128 128 log.error("Git client import failed: %s", git_import_err)
129 129
130 130 if MercurialFactory and HgRemote:
131 131 hg_factory = MercurialFactory()
132 132 self._hg_remote = HgRemote(hg_factory)
133 133 else:
134 134 log.error("Mercurial client import failed: %s", hg_import_err)
135 135
136 136 if SubversionFactory and SvnRemote:
137 137 svn_factory = SubversionFactory()
138 138
139 139 # hg factory is used for svn url validation
140 140 hg_factory = MercurialFactory()
141 141 self._svn_remote = SvnRemote(svn_factory, hg_factory=hg_factory)
142 142 else:
143 143 log.error("Subversion client import failed: %s", svn_import_err)
144 144
145 145 self._vcsserver = VcsServer()
146 146
147 147 def _configure_locale(self):
148 148 if self.locale:
149 149 log.info('Settings locale: `LC_ALL` to %s', self.locale)
150 150 else:
151 151 log.info('Configuring locale subsystem based on environment variables')
152 152 try:
153 153 # If self.locale is the empty string, then the locale
154 154 # module will use the environment variables. See the
155 155 # documentation of the package `locale`.
156 156 locale.setlocale(locale.LC_ALL, self.locale)
157 157
158 158 language_code, encoding = locale.getlocale()
159 159 log.info(
160 160 'Locale set to language code "%s" with encoding "%s".',
161 161 language_code, encoding)
162 162 except locale.Error:
163 163 log.exception('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' % (list(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
230 230 self.config = Configurator(settings=settings)
231 231 # Init our statsd at very start
232 232 self.config.registry.statsd = StatsdClient.statsd
233 233 self.config.registry.vcs_call_context = {}
234 234
235 235 self.global_config = global_config
236 236 self.config.include('vcsserver.lib.rc_cache')
237 237
238 238 settings_locale = settings.get('locale', '') or 'en_US.UTF-8'
239 239 vcs = VCS(locale_conf=settings_locale, cache_config=settings)
240 240 self._remotes = {
241 241 'hg': vcs._hg_remote,
242 242 'git': vcs._git_remote,
243 243 'svn': vcs._svn_remote,
244 244 'server': vcs._vcsserver,
245 245 }
246 246 if settings.get('dev.use_echo_app', 'false').lower() == 'true':
247 247 self._use_echo_app = True
248 248 log.warning("Using EchoApp for VCS operations.")
249 249 self.remote_wsgi = remote_wsgi_stub
250 250
251 251 self._configure_settings(global_config, settings)
252 252
253 253 self._configure()
254 254
255 255 def _configure_settings(self, global_config, app_settings):
256 256 """
257 257 Configure the settings module.
258 258 """
259 259 settings_merged = global_config.copy()
260 260 settings_merged.update(app_settings)
261 261
262 262 git_path = app_settings.get('git_path', None)
263 263 if git_path:
264 264 settings.GIT_EXECUTABLE = git_path
265 265 binary_dir = app_settings.get('core.binary_dir', None)
266 266 if binary_dir:
267 267 settings.BINARY_DIR = binary_dir
268 268
269 269 # Store the settings to make them available to other modules.
270 270 vcsserver.PYRAMID_SETTINGS = settings_merged
271 271 vcsserver.CONFIG = settings_merged
272 272
273 273 def _configure(self):
274 274 self.config.add_renderer(name='msgpack', factory=self._msgpack_renderer_factory)
275 275
276 276 self.config.add_route('service', '/_service')
277 277 self.config.add_route('status', '/status')
278 278 self.config.add_route('hg_proxy', '/proxy/hg')
279 279 self.config.add_route('git_proxy', '/proxy/git')
280 280
281 281 # rpc methods
282 282 self.config.add_route('vcs', '/{backend}')
283 283
284 284 # streaming rpc remote methods
285 285 self.config.add_route('vcs_stream', '/{backend}/stream')
286 286
287 287 # vcs operations clone/push as streaming
288 288 self.config.add_route('stream_git', '/stream/git/*repo_name')
289 289 self.config.add_route('stream_hg', '/stream/hg/*repo_name')
290 290
291 291 self.config.add_view(self.status_view, route_name='status', renderer='json')
292 292 self.config.add_view(self.service_view, route_name='service', renderer='msgpack')
293 293
294 294 self.config.add_view(self.hg_proxy(), route_name='hg_proxy')
295 295 self.config.add_view(self.git_proxy(), route_name='git_proxy')
296 296 self.config.add_view(self.vcs_view, route_name='vcs', renderer='msgpack',
297 297 vcs_view=self._remotes)
298 298 self.config.add_view(self.vcs_stream_view, route_name='vcs_stream',
299 299 vcs_view=self._remotes)
300 300
301 301 self.config.add_view(self.hg_stream(), route_name='stream_hg')
302 302 self.config.add_view(self.git_stream(), route_name='stream_git')
303 303
304 304 self.config.add_view_predicate('vcs_view', VCSViewPredicate)
305 305
306 306 self.config.add_notfound_view(not_found, renderer='json')
307 307
308 308 self.config.add_view(self.handle_vcs_exception, context=Exception)
309 309
310 310 self.config.add_tween(
311 311 'vcsserver.tweens.request_wrapper.RequestWrapperTween',
312 312 )
313 313 self.config.add_request_method(
314 314 'vcsserver.lib.request_counter.get_request_counter',
315 315 'request_count')
316 316
317 317 def wsgi_app(self):
318 318 return self.config.make_wsgi_app()
319 319
320 320 def _vcs_view_params(self, request):
321 321 remote = self._remotes[request.matchdict['backend']]
322 322 payload = msgpack.unpackb(request.body, use_list=True)
323 323
324 324 method = payload.get('method')
325 325 params = payload['params']
326 326 wire = params.get('wire')
327 327 args = params.get('args')
328 328 kwargs = params.get('kwargs')
329 329 context_uid = None
330 330
331 331 request.registry.vcs_call_context = {
332 332 'method': method,
333 333 'repo_name': payload.get('_repo_name')
334 334 }
335 335
336 336 if wire:
337 337 try:
338 338 wire['context'] = context_uid = uuid.UUID(wire['context'])
339 339 except KeyError:
340 340 pass
341 341 args.insert(0, wire)
342 342 repo_state_uid = wire.get('repo_state_uid') if wire else None
343 343
344 344 # NOTE(marcink): trading complexity for slight performance
345 345 if log.isEnabledFor(logging.DEBUG):
346 346 # also we SKIP printing out any of those methods args since they maybe excessive
347 347 just_args_methods = {
348 348 'commitctx': ('content', 'removed', 'updated')
349 349 }
350 350 if method in just_args_methods:
351 351 skip_args = just_args_methods[method]
352 352 call_args = ''
353 353 call_kwargs = {}
354 354 for k in kwargs:
355 355 if k in skip_args:
356 356 # replace our skip key with dummy
357 357 call_kwargs[k] = f'RemovedParam({k})'
358 358 else:
359 359 call_kwargs[k] = kwargs[k]
360 360 else:
361 361 call_args = args[1:]
362 362 call_kwargs = kwargs
363 363
364 364 log.debug('Method requested:`%s` with args:%s kwargs:%s context_uid: %s, repo_state_uid:%s',
365 365 method, call_args, call_kwargs, context_uid, repo_state_uid)
366 366
367 367 statsd = request.registry.statsd
368 368 if statsd:
369 369 statsd.incr(
370 370 'vcsserver_method_total', tags=[
371 371 "method:{}".format(method),
372 372 ])
373 373 return payload, remote, method, args, kwargs
374 374
375 375 def vcs_view(self, request):
376 376
377 377 payload, remote, method, args, kwargs = self._vcs_view_params(request)
378 378 payload_id = payload.get('id')
379 379
380 380 try:
381 381 resp = getattr(remote, method)(*args, **kwargs)
382 382 except Exception as e:
383 383 exc_info = list(sys.exc_info())
384 384 exc_type, exc_value, exc_traceback = exc_info
385 385
386 386 org_exc = getattr(e, '_org_exc', None)
387 387 org_exc_name = None
388 388 org_exc_tb = ''
389 389 if org_exc:
390 390 org_exc_name = org_exc.__class__.__name__
391 391 org_exc_tb = getattr(e, '_org_exc_tb', '')
392 392 # replace our "faked" exception with our org
393 393 exc_info[0] = org_exc.__class__
394 394 exc_info[1] = org_exc
395 395
396 396 should_store_exc = True
397 397 if org_exc:
398 398 def get_exc_fqn(_exc_obj):
399 399 module_name = getattr(org_exc.__class__, '__module__', 'UNKNOWN')
400 400 return module_name + '.' + org_exc_name
401 401
402 402 exc_fqn = get_exc_fqn(org_exc)
403 403
404 404 if exc_fqn in ['mercurial.error.RepoLookupError',
405 405 'vcsserver.exceptions.RefNotFoundException']:
406 406 should_store_exc = False
407 407
408 408 if should_store_exc:
409 409 store_exception(id(exc_info), exc_info, request_path=request.path)
410 410
411 411 tb_info = ''.join(
412 412 traceback.format_exception(exc_type, exc_value, exc_traceback))
413 413
414 414 type_ = e.__class__.__name__
415 415 if type_ not in self.ALLOWED_EXCEPTIONS:
416 416 type_ = None
417 417
418 418 resp = {
419 419 'id': payload_id,
420 420 'error': {
421 421 'message': str(e),
422 422 'traceback': tb_info,
423 423 'org_exc': org_exc_name,
424 424 'org_exc_tb': org_exc_tb,
425 425 'type': type_
426 426 }
427 427 }
428 428
429 429 try:
430 430 resp['error']['_vcs_kind'] = getattr(e, '_vcs_kind', None)
431 431 except AttributeError:
432 432 pass
433 433 else:
434 434 resp = {
435 435 'id': payload_id,
436 436 'result': resp
437 437 }
438 438 log.debug('Serving data for method %s', method)
439 439 return resp
440 440
441 441 def vcs_stream_view(self, request):
442 442 payload, remote, method, args, kwargs = self._vcs_view_params(request)
443 443 # this method has a stream: marker we remove it here
444 444 method = method.split('stream:')[-1]
445 445 chunk_size = safe_int(payload.get('chunk_size')) or 4096
446 446
447 447 try:
448 448 resp = getattr(remote, method)(*args, **kwargs)
449 449 except Exception as e:
450 450 raise
451 451
452 452 def get_chunked_data(method_resp):
453 453 stream = io.BytesIO(method_resp)
454 454 while 1:
455 455 chunk = stream.read(chunk_size)
456 456 if not chunk:
457 457 break
458 458 yield chunk
459 459
460 460 response = Response(app_iter=get_chunked_data(resp))
461 461 response.content_type = 'application/octet-stream'
462 462
463 463 return response
464 464
465 465 def status_view(self, request):
466 466 import vcsserver
467 return {'status': 'OK', 'vcsserver_version': safe_str(vcsserver.__version__),
467 return {'status': 'OK', 'vcsserver_version': vcsserver.__version__,
468 468 'pid': os.getpid()}
469 469
470 470 def service_view(self, request):
471 471 import vcsserver
472 472
473 473 payload = msgpack.unpackb(request.body, use_list=True)
474 474 server_config, app_config = {}, {}
475 475
476 476 try:
477 477 path = self.global_config['__file__']
478 478 config = configparser.RawConfigParser()
479 479
480 480 config.read(path)
481 481
482 482 if config.has_section('server:main'):
483 483 server_config = dict(config.items('server:main'))
484 484 if config.has_section('app:main'):
485 485 app_config = dict(config.items('app:main'))
486 486
487 487 except Exception:
488 488 log.exception('Failed to read .ini file for display')
489 489
490 490 environ = list(os.environ.items())
491 491
492 492 resp = {
493 493 'id': payload.get('id'),
494 494 'result': dict(
495 version=safe_str(vcsserver.__version__),
495 version=vcsserver.__version__,
496 496 config=server_config,
497 497 app_config=app_config,
498 498 environ=environ,
499 499 payload=payload,
500 500 )
501 501 }
502 502 return resp
503 503
504 504 def _msgpack_renderer_factory(self, info):
505 505
506 506 def _render(value, system):
507 507 bin_type = False
508 508 res = value.get('result')
509 509 if res and isinstance(res, BinaryEnvelope):
510 510 value['result'] = res.value
511 511 bin_type = res.bin_type
512 512
513 513 request = system.get('request')
514 514 if request is not None:
515 515 response = request.response
516 516 ct = response.content_type
517 517 if ct == response.default_content_type:
518 518 response.content_type = 'application/x-msgpack'
519 519
520 520 return msgpack.packb(value, use_bin_type=bin_type)
521 521 return _render
522 522
523 523 def set_env_from_config(self, environ, config):
524 524 dict_conf = {}
525 525 try:
526 526 for elem in config:
527 527 if elem[0] == 'rhodecode':
528 528 dict_conf = json.loads(elem[2])
529 529 break
530 530 except Exception:
531 531 log.exception('Failed to fetch SCM CONFIG')
532 532 return
533 533
534 534 username = dict_conf.get('username')
535 535 if username:
536 536 environ['REMOTE_USER'] = username
537 537 # mercurial specific, some extension api rely on this
538 538 environ['HGUSER'] = username
539 539
540 540 ip = dict_conf.get('ip')
541 541 if ip:
542 542 environ['REMOTE_HOST'] = ip
543 543
544 544 if _is_request_chunked(environ):
545 545 # set the compatibility flag for webob
546 546 environ['wsgi.input_terminated'] = True
547 547
548 548 def hg_proxy(self):
549 549 @wsgiapp
550 550 def _hg_proxy(environ, start_response):
551 551 app = WsgiProxy(self.remote_wsgi.HgRemoteWsgi())
552 552 return app(environ, start_response)
553 553 return _hg_proxy
554 554
555 555 def git_proxy(self):
556 556 @wsgiapp
557 557 def _git_proxy(environ, start_response):
558 558 app = WsgiProxy(self.remote_wsgi.GitRemoteWsgi())
559 559 return app(environ, start_response)
560 560 return _git_proxy
561 561
562 562 def hg_stream(self):
563 563 if self._use_echo_app:
564 564 @wsgiapp
565 565 def _hg_stream(environ, start_response):
566 566 app = EchoApp('fake_path', 'fake_name', None)
567 567 return app(environ, start_response)
568 568 return _hg_stream
569 569 else:
570 570 @wsgiapp
571 571 def _hg_stream(environ, start_response):
572 572 log.debug('http-app: handling hg stream')
573 573
574 574 packed_cc = base64.b64decode(environ['HTTP_X_RC_VCS_STREAM_CALL_CONTEXT'])
575 575 call_context = msgpack.unpackb(packed_cc)
576 576
577 577 repo_path = call_context['repo_path']
578 578 repo_name = call_context['repo_name']
579 579 config = call_context['repo_config']
580 580
581 581 app = scm_app.create_hg_wsgi_app(
582 582 repo_path, repo_name, config)
583 583
584 584 # Consistent path information for hgweb
585 585 environ['PATH_INFO'] = call_context['path_info']
586 586 environ['REPO_NAME'] = repo_name
587 587 self.set_env_from_config(environ, config)
588 588
589 589 log.debug('http-app: starting app handler '
590 590 'with %s and process request', app)
591 591 return app(environ, ResponseFilter(start_response))
592 592 return _hg_stream
593 593
594 594 def git_stream(self):
595 595 if self._use_echo_app:
596 596 @wsgiapp
597 597 def _git_stream(environ, start_response):
598 598 app = EchoApp('fake_path', 'fake_name', None)
599 599 return app(environ, start_response)
600 600 return _git_stream
601 601 else:
602 602 @wsgiapp
603 603 def _git_stream(environ, start_response):
604 604 log.debug('http-app: handling git stream')
605 605
606 606 packed_cc = base64.b64decode(environ['HTTP_X_RC_VCS_STREAM_CALL_CONTEXT'])
607 607 call_context = msgpack.unpackb(packed_cc)
608 608
609 609 repo_path = call_context['repo_path']
610 610 repo_name = call_context['repo_name']
611 611 config = call_context['repo_config']
612 612
613 613 environ['PATH_INFO'] = call_context['path_info']
614 614 self.set_env_from_config(environ, config)
615 615
616 616 content_type = environ.get('CONTENT_TYPE', '')
617 617
618 618 path = environ['PATH_INFO']
619 619 is_lfs_request = GIT_LFS_CONTENT_TYPE in content_type
620 620 log.debug(
621 621 'LFS: Detecting if request `%s` is LFS server path based '
622 622 'on content type:`%s`, is_lfs:%s',
623 623 path, content_type, is_lfs_request)
624 624
625 625 if not is_lfs_request:
626 626 # fallback detection by path
627 627 if GIT_LFS_PROTO_PAT.match(path):
628 628 is_lfs_request = True
629 629 log.debug(
630 630 'LFS: fallback detection by path of: `%s`, is_lfs:%s',
631 631 path, is_lfs_request)
632 632
633 633 if is_lfs_request:
634 634 app = scm_app.create_git_lfs_wsgi_app(
635 635 repo_path, repo_name, config)
636 636 else:
637 637 app = scm_app.create_git_wsgi_app(
638 638 repo_path, repo_name, config)
639 639
640 640 log.debug('http-app: starting app handler '
641 641 'with %s and process request', app)
642 642
643 643 return app(environ, start_response)
644 644
645 645 return _git_stream
646 646
647 647 def handle_vcs_exception(self, exception, request):
648 648 _vcs_kind = getattr(exception, '_vcs_kind', '')
649 649 if _vcs_kind == 'repo_locked':
650 650 # Get custom repo-locked status code if present.
651 651 status_code = request.headers.get('X-RC-Locked-Status-Code')
652 652 return HTTPRepoLocked(
653 653 title=exception.message, status_code=status_code)
654 654
655 655 elif _vcs_kind == 'repo_branch_protected':
656 656 # Get custom repo-branch-protected status code if present.
657 657 return HTTPRepoBranchProtected(title=exception.message)
658 658
659 659 exc_info = request.exc_info
660 660 store_exception(id(exc_info), exc_info)
661 661
662 662 traceback_info = 'unavailable'
663 663 if request.exc_info:
664 664 exc_type, exc_value, exc_tb = request.exc_info
665 665 traceback_info = ''.join(traceback.format_exception(exc_type, exc_value, exc_tb))
666 666
667 667 log.error(
668 668 'error occurred handling this request for path: %s, \n tb: %s',
669 669 request.path, traceback_info)
670 670
671 671 statsd = request.registry.statsd
672 672 if statsd:
673 673 exc_type = "{}.{}".format(exception.__class__.__module__, exception.__class__.__name__)
674 674 statsd.incr('vcsserver_exception_total',
675 675 tags=["type:{}".format(exc_type)])
676 676 raise exception
677 677
678 678
679 679 class ResponseFilter(object):
680 680
681 681 def __init__(self, start_response):
682 682 self._start_response = start_response
683 683
684 684 def __call__(self, status, response_headers, exc_info=None):
685 685 headers = tuple(
686 686 (h, v) for h, v in response_headers
687 687 if not wsgiref.util.is_hop_by_hop(h))
688 688 return self._start_response(status, headers, exc_info)
689 689
690 690
691 691 def sanitize_settings_and_apply_defaults(global_config, settings):
692 692 global_settings_maker = SettingsMaker(global_config)
693 693 settings_maker = SettingsMaker(settings)
694 694
695 695 settings_maker.make_setting('logging.autoconfigure', False, parser='bool')
696 696
697 697 logging_conf = os.path.join(os.path.dirname(global_config.get('__file__')), 'logging.ini')
698 698 settings_maker.enable_logging(logging_conf)
699 699
700 700 # Default includes, possible to change as a user
701 701 pyramid_includes = settings_maker.make_setting('pyramid.includes', [], parser='list:newline')
702 702 log.debug("Using the following pyramid.includes: %s", pyramid_includes)
703 703
704 704 settings_maker.make_setting('__file__', global_config.get('__file__'))
705 705
706 706 settings_maker.make_setting('pyramid.default_locale_name', 'en')
707 707 settings_maker.make_setting('locale', 'en_US.UTF-8')
708 708
709 709 settings_maker.make_setting('core.binary_dir', '')
710 710
711 711 temp_store = tempfile.gettempdir()
712 712 default_cache_dir = os.path.join(temp_store, 'rc_cache')
713 713 # save default, cache dir, and use it for all backends later.
714 714 default_cache_dir = settings_maker.make_setting(
715 715 'cache_dir',
716 716 default=default_cache_dir, default_when_empty=True,
717 717 parser='dir:ensured')
718 718
719 719 # exception store cache
720 720 settings_maker.make_setting(
721 721 'exception_tracker.store_path',
722 722 default=os.path.join(default_cache_dir, 'exc_store'), default_when_empty=True,
723 723 parser='dir:ensured'
724 724 )
725 725
726 726 # repo_object cache defaults
727 727 settings_maker.make_setting(
728 728 'rc_cache.repo_object.backend',
729 729 default='dogpile.cache.rc.file_namespace',
730 730 parser='string')
731 731 settings_maker.make_setting(
732 732 'rc_cache.repo_object.expiration_time',
733 733 default=30 * 24 * 60 * 60, # 30days
734 734 parser='int')
735 735 settings_maker.make_setting(
736 736 'rc_cache.repo_object.arguments.filename',
737 737 default=os.path.join(default_cache_dir, 'vcsserver_cache_repo_object.db'),
738 738 parser='string')
739 739
740 740 # statsd
741 741 settings_maker.make_setting('statsd.enabled', False, parser='bool')
742 742 settings_maker.make_setting('statsd.statsd_host', 'statsd-exporter', parser='string')
743 743 settings_maker.make_setting('statsd.statsd_port', 9125, parser='int')
744 744 settings_maker.make_setting('statsd.statsd_prefix', '')
745 745 settings_maker.make_setting('statsd.statsd_ipv6', False, parser='bool')
746 746
747 747 settings_maker.env_expand()
748 748
749 749
750 750 def main(global_config, **settings):
751 751 start_time = time.time()
752 752 log.info('Pyramid app config starting')
753 753
754 754 if MercurialFactory:
755 755 hgpatches.patch_largefiles_capabilities()
756 756 hgpatches.patch_subrepo_type_mapping()
757 757
758 758 # Fill in and sanitize the defaults & do ENV expansion
759 759 sanitize_settings_and_apply_defaults(global_config, settings)
760 760
761 761 # init and bootstrap StatsdClient
762 762 StatsdClient.setup(settings)
763 763
764 764 pyramid_app = HTTPApplication(settings=settings, global_config=global_config).wsgi_app()
765 765 total_time = time.time() - start_time
766 766 log.info('Pyramid app `%s` created and configured in %.2fs',
767 767 getattr(pyramid_app, 'func_name', 'pyramid_app'), total_time)
768 768 return pyramid_app
769 769
770 770
@@ -1,206 +1,206 b''
1 1 # RhodeCode VCSServer provides access to different vcs backends via network.
2 2 # Copyright (C) 2014-2020 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 stat
21 21 import pytest
22 22 import vcsserver
23 23 import tempfile
24 24 from vcsserver import hook_utils
25 25 from vcsserver.tests.fixture import no_newline_id_generator
26 26 from vcsserver.str_utils import safe_bytes, safe_str
27 27 from vcsserver.utils import AttributeDict
28 28
29 29
30 30 class TestCheckRhodecodeHook(object):
31 31
32 32 def test_returns_false_when_hook_file_is_wrong_found(self, tmpdir):
33 33 hook = os.path.join(str(tmpdir), 'fake_hook_file.py')
34 34 with open(hook, 'wb') as f:
35 35 f.write(b'dummy test')
36 36 result = hook_utils.check_rhodecode_hook(hook)
37 37 assert result is False
38 38
39 39 def test_returns_true_when_no_hook_file_found(self, tmpdir):
40 40 hook = os.path.join(str(tmpdir), 'fake_hook_file_not_existing.py')
41 41 result = hook_utils.check_rhodecode_hook(hook)
42 42 assert result
43 43
44 44 @pytest.mark.parametrize("file_content, expected_result", [
45 45 ("RC_HOOK_VER = '3.3.3'\n", True),
46 46 ("RC_HOOK = '3.3.3'\n", False),
47 47 ], ids=no_newline_id_generator)
48 48 def test_signatures(self, file_content, expected_result, tmpdir):
49 49 hook = os.path.join(str(tmpdir), 'fake_hook_file_1.py')
50 50 with open(hook, 'wb') as f:
51 51 f.write(safe_bytes(file_content))
52 52
53 53 result = hook_utils.check_rhodecode_hook(hook)
54 54
55 55 assert result is expected_result
56 56
57 57
58 58 class BaseInstallHooks(object):
59 59 HOOK_FILES = ()
60 60
61 61 def _check_hook_file_mode(self, file_path):
62 62 assert os.path.exists(file_path), 'path %s missing' % file_path
63 63 stat_info = os.stat(file_path)
64 64
65 65 file_mode = stat.S_IMODE(stat_info.st_mode)
66 66 expected_mode = int('755', 8)
67 67 assert expected_mode == file_mode
68 68
69 69 def _check_hook_file_content(self, file_path, executable):
70 70 executable = executable or sys.executable
71 71 with open(file_path, 'rt') as hook_file:
72 72 content = hook_file.read()
73 73
74 74 expected_env = '#!{}'.format(executable)
75 expected_rc_version = "\nRC_HOOK_VER = '{}'\n".format(safe_str(vcsserver.__version__))
75 expected_rc_version = "\nRC_HOOK_VER = '{}'\n".format(vcsserver.__version__)
76 76 assert content.strip().startswith(expected_env)
77 77 assert expected_rc_version in content
78 78
79 79 def _create_fake_hook(self, file_path, content):
80 80 with open(file_path, 'w') as hook_file:
81 81 hook_file.write(content)
82 82
83 83 def create_dummy_repo(self, repo_type):
84 84 tmpdir = tempfile.mkdtemp()
85 85 repo = AttributeDict()
86 86 if repo_type == 'git':
87 87 repo.path = os.path.join(tmpdir, 'test_git_hooks_installation_repo')
88 88 os.makedirs(repo.path)
89 89 os.makedirs(os.path.join(repo.path, 'hooks'))
90 90 repo.bare = True
91 91
92 92 elif repo_type == 'svn':
93 93 repo.path = os.path.join(tmpdir, 'test_svn_hooks_installation_repo')
94 94 os.makedirs(repo.path)
95 95 os.makedirs(os.path.join(repo.path, 'hooks'))
96 96
97 97 return repo
98 98
99 99 def check_hooks(self, repo_path, repo_bare=True):
100 100 for file_name in self.HOOK_FILES:
101 101 if repo_bare:
102 102 file_path = os.path.join(repo_path, 'hooks', file_name)
103 103 else:
104 104 file_path = os.path.join(repo_path, '.git', 'hooks', file_name)
105 105 self._check_hook_file_mode(file_path)
106 106 self._check_hook_file_content(file_path, sys.executable)
107 107
108 108
109 109 class TestInstallGitHooks(BaseInstallHooks):
110 110 HOOK_FILES = ('pre-receive', 'post-receive')
111 111
112 112 def test_hooks_are_installed(self):
113 113 repo = self.create_dummy_repo('git')
114 114 result = hook_utils.install_git_hooks(repo.path, repo.bare)
115 115 assert result
116 116 self.check_hooks(repo.path, repo.bare)
117 117
118 118 def test_hooks_are_replaced(self):
119 119 repo = self.create_dummy_repo('git')
120 120 hooks_path = os.path.join(repo.path, 'hooks')
121 121 for file_path in [os.path.join(hooks_path, f) for f in self.HOOK_FILES]:
122 122 self._create_fake_hook(
123 123 file_path, content="RC_HOOK_VER = 'abcde'\n")
124 124
125 125 result = hook_utils.install_git_hooks(repo.path, repo.bare)
126 126 assert result
127 127 self.check_hooks(repo.path, repo.bare)
128 128
129 129 def test_non_rc_hooks_are_not_replaced(self):
130 130 repo = self.create_dummy_repo('git')
131 131 hooks_path = os.path.join(repo.path, 'hooks')
132 132 non_rc_content = 'echo "non rc hook"\n'
133 133 for file_path in [os.path.join(hooks_path, f) for f in self.HOOK_FILES]:
134 134 self._create_fake_hook(
135 135 file_path, content=non_rc_content)
136 136
137 137 result = hook_utils.install_git_hooks(repo.path, repo.bare)
138 138 assert result
139 139
140 140 for file_path in [os.path.join(hooks_path, f) for f in self.HOOK_FILES]:
141 141 with open(file_path, 'rt') as hook_file:
142 142 content = hook_file.read()
143 143 assert content == non_rc_content
144 144
145 145 def test_non_rc_hooks_are_replaced_with_force_flag(self):
146 146 repo = self.create_dummy_repo('git')
147 147 hooks_path = os.path.join(repo.path, 'hooks')
148 148 non_rc_content = 'echo "non rc hook"\n'
149 149 for file_path in [os.path.join(hooks_path, f) for f in self.HOOK_FILES]:
150 150 self._create_fake_hook(
151 151 file_path, content=non_rc_content)
152 152
153 153 result = hook_utils.install_git_hooks(
154 154 repo.path, repo.bare, force_create=True)
155 155 assert result
156 156 self.check_hooks(repo.path, repo.bare)
157 157
158 158
159 159 class TestInstallSvnHooks(BaseInstallHooks):
160 160 HOOK_FILES = ('pre-commit', 'post-commit')
161 161
162 162 def test_hooks_are_installed(self):
163 163 repo = self.create_dummy_repo('svn')
164 164 result = hook_utils.install_svn_hooks(repo.path)
165 165 assert result
166 166 self.check_hooks(repo.path)
167 167
168 168 def test_hooks_are_replaced(self):
169 169 repo = self.create_dummy_repo('svn')
170 170 hooks_path = os.path.join(repo.path, 'hooks')
171 171 for file_path in [os.path.join(hooks_path, f) for f in self.HOOK_FILES]:
172 172 self._create_fake_hook(
173 173 file_path, content="RC_HOOK_VER = 'abcde'\n")
174 174
175 175 result = hook_utils.install_svn_hooks(repo.path)
176 176 assert result
177 177 self.check_hooks(repo.path)
178 178
179 179 def test_non_rc_hooks_are_not_replaced(self):
180 180 repo = self.create_dummy_repo('svn')
181 181 hooks_path = os.path.join(repo.path, 'hooks')
182 182 non_rc_content = 'echo "non rc hook"\n'
183 183 for file_path in [os.path.join(hooks_path, f) for f in self.HOOK_FILES]:
184 184 self._create_fake_hook(
185 185 file_path, content=non_rc_content)
186 186
187 187 result = hook_utils.install_svn_hooks(repo.path)
188 188 assert result
189 189
190 190 for file_path in [os.path.join(hooks_path, f) for f in self.HOOK_FILES]:
191 191 with open(file_path, 'rt') as hook_file:
192 192 content = hook_file.read()
193 193 assert content == non_rc_content
194 194
195 195 def test_non_rc_hooks_are_replaced_with_force_flag(self):
196 196 repo = self.create_dummy_repo('svn')
197 197 hooks_path = os.path.join(repo.path, 'hooks')
198 198 non_rc_content = 'echo "non rc hook"\n'
199 199 for file_path in [os.path.join(hooks_path, f) for f in self.HOOK_FILES]:
200 200 self._create_fake_hook(
201 201 file_path, content=non_rc_content)
202 202
203 203 result = hook_utils.install_svn_hooks(
204 204 repo.path, force_create=True)
205 205 assert result
206 206 self.check_hooks(repo.path, )
@@ -1,112 +1,112 b''
1 1 # RhodeCode VCSServer provides access to different vcs backends via network.
2 2 # Copyright (C) 2014-2020 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 time
19 19 import logging
20 20
21 21 import vcsserver
22 22 from vcsserver.str_utils import safe_str, ascii_str
23 23
24 24 log = logging.getLogger(__name__)
25 25
26 26
27 27 def get_access_path(environ):
28 28 path = environ.get('PATH_INFO')
29 29 return path
30 30
31 31
32 32 def get_user_agent(environ):
33 33 return environ.get('HTTP_USER_AGENT')
34 34
35 35
36 36 def get_call_context(registry) -> dict:
37 37 cc = {}
38 38 if hasattr(registry, 'vcs_call_context'):
39 39 cc.update({
40 40 'X-RC-Method': registry.vcs_call_context.get('method'),
41 41 'X-RC-Repo-Name': registry.vcs_call_context.get('repo_name')
42 42 })
43 43
44 44 return cc
45 45
46 46
47 47 class RequestWrapperTween(object):
48 48 def __init__(self, handler, registry):
49 49 self.handler = handler
50 50 self.registry = registry
51 51
52 52 # one-time configuration code goes here
53 53
54 54 def __call__(self, request):
55 55 start = time.time()
56 56 log.debug('Starting request time measurement')
57 57 response = None
58 58
59 59 try:
60 60 response = self.handler(request)
61 61 finally:
62 62 ua = get_user_agent(request.environ)
63 63 call_context = get_call_context(request.registry)
64 64 vcs_method = call_context.get('X-RC-Method', '_NO_VCS_METHOD')
65 65 repo_name = call_context.get('X-RC-Repo-Name', '')
66 66
67 67 count = request.request_count()
68 _ver_ = ascii_str(vcsserver.__version__)
68 _ver_ = vcsserver.__version__
69 69 _path = safe_str(get_access_path(request.environ))
70 70
71 71 ip = '127.0.0.1'
72 72 match_route = request.matched_route.name if request.matched_route else "NOT_FOUND"
73 73 resp_code = getattr(response, 'status_code', 'UNDEFINED')
74 74
75 75 _view_path = f"{repo_name}@{_path}/{vcs_method}"
76 76
77 77 total = time.time() - start
78 78
79 79 log.info(
80 80 'Req[%4s] IP: %s %s Request to %s time: %.4fs [%s], VCSServer %s',
81 81 count, ip, request.environ.get('REQUEST_METHOD'),
82 82 _view_path, total, ua, _ver_,
83 83 extra={"time": total, "ver": _ver_, "code": resp_code,
84 84 "path": _path, "view_name": match_route, "user_agent": ua,
85 85 "vcs_method": vcs_method, "repo_name": repo_name}
86 86 )
87 87
88 88 statsd = request.registry.statsd
89 89 if statsd:
90 90 match_route = request.matched_route.name if request.matched_route else _path
91 91 elapsed_time_ms = round(1000.0 * total) # use ms only
92 92 statsd.timing(
93 93 "vcsserver_req_timing.histogram", elapsed_time_ms,
94 94 tags=[
95 95 "view_name:{}".format(match_route),
96 96 "code:{}".format(resp_code)
97 97 ],
98 98 use_decimals=False
99 99 )
100 100 statsd.incr(
101 101 "vcsserver_req_total", tags=[
102 102 "view_name:{}".format(match_route),
103 103 "code:{}".format(resp_code)
104 104 ])
105 105
106 106 return response
107 107
108 108
109 109 def includeme(config):
110 110 config.add_tween(
111 111 'vcsserver.tweens.request_wrapper.RequestWrapperTween',
112 112 )
1 NO CONTENT: file was removed
General Comments 0
You need to be logged in to leave comments. Login now