##// END OF EJS Templates
requests: cleaned / unified way of handling requests generation from non-web scope....
super-admin -
r4873:d49e3bd8 default
parent child Browse files
Show More
@@ -1,619 +1,610 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2010-2020 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
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 Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 import os
22 22 import sys
23 23 import collections
24 24 import tempfile
25 25 import time
26 26 import logging.config
27 27
28 28 from paste.gzipper import make_gzip_middleware
29 29 import pyramid.events
30 30 from pyramid.wsgi import wsgiapp
31 31 from pyramid.authorization import ACLAuthorizationPolicy
32 32 from pyramid.config import Configurator
33 33 from pyramid.settings import asbool, aslist
34 34 from pyramid.httpexceptions import (
35 35 HTTPException, HTTPError, HTTPInternalServerError, HTTPFound, HTTPNotFound)
36 36 from pyramid.renderers import render_to_response
37 37
38 38 from rhodecode.model import meta
39 39 from rhodecode.config import patches
40 40 from rhodecode.config import utils as config_utils
41 41 from rhodecode.config.settings_maker import SettingsMaker
42 42 from rhodecode.config.environment import load_pyramid_environment
43 43
44 44 import rhodecode.events
45 45 from rhodecode.lib.middleware.vcs import VCSMiddleware
46 46 from rhodecode.lib.request import Request
47 47 from rhodecode.lib.vcs import VCSCommunicationError
48 48 from rhodecode.lib.exceptions import VCSServerUnavailable
49 49 from rhodecode.lib.middleware.appenlight import wrap_in_appenlight_if_enabled
50 50 from rhodecode.lib.middleware.https_fixup import HttpsFixup
51 51 from rhodecode.lib.plugins.utils import register_rhodecode_plugin
52 52 from rhodecode.lib.utils2 import AttributeDict
53 53 from rhodecode.lib.exc_tracking import store_exception
54 54 from rhodecode.subscribers import (
55 55 scan_repositories_if_enabled, write_js_routes_if_enabled,
56 56 write_metadata_if_needed, write_usage_data)
57 57 from rhodecode.lib.statsd_client import StatsdClient
58 58
59 59 log = logging.getLogger(__name__)
60 60
61 61
62 62 def is_http_error(response):
63 63 # error which should have traceback
64 64 return response.status_code > 499
65 65
66 66
67 67 def should_load_all():
68 68 """
69 69 Returns if all application components should be loaded. In some cases it's
70 70 desired to skip apps loading for faster shell script execution
71 71 """
72 72 ssh_cmd = os.environ.get('RC_CMD_SSH_WRAPPER')
73 73 if ssh_cmd:
74 74 return False
75 75
76 76 return True
77 77
78 78
79 79 def make_pyramid_app(global_config, **settings):
80 80 """
81 81 Constructs the WSGI application based on Pyramid.
82 82
83 83 Specials:
84 84
85 85 * The application can also be integrated like a plugin via the call to
86 86 `includeme`. This is accompanied with the other utility functions which
87 87 are called. Changing this should be done with great care to not break
88 88 cases when these fragments are assembled from another place.
89 89
90 90 """
91 91 start_time = time.time()
92 92 log.info('Pyramid app config starting')
93 93
94 94 sanitize_settings_and_apply_defaults(global_config, settings)
95 95
96 96 # init and bootstrap StatsdClient
97 97 StatsdClient.setup(settings)
98 98
99 99 config = Configurator(settings=settings)
100 100 # Init our statsd at very start
101 101 config.registry.statsd = StatsdClient.statsd
102 102
103 103 # Apply compatibility patches
104 104 patches.inspect_getargspec()
105 105
106 106 load_pyramid_environment(global_config, settings)
107 107
108 108 # Static file view comes first
109 109 includeme_first(config)
110 110
111 111 includeme(config)
112 112
113 113 pyramid_app = config.make_wsgi_app()
114 114 pyramid_app = wrap_app_in_wsgi_middlewares(pyramid_app, config)
115 115 pyramid_app.config = config
116 116
117 117 celery_settings = get_celery_config(settings)
118 118 config.configure_celery(celery_settings)
119 119
120 120 # creating the app uses a connection - return it after we are done
121 121 meta.Session.remove()
122 122
123 123 total_time = time.time() - start_time
124 124 log.info('Pyramid app `%s` created and configured in %.2fs',
125 125 getattr(pyramid_app, 'func_name', 'pyramid_app'), total_time)
126 126 return pyramid_app
127 127
128 128
129 129 def get_celery_config(settings):
130 130 """
131 131 Converts basic ini configuration into celery 4.X options
132 132 """
133 133
134 134 def key_converter(key_name):
135 135 pref = 'celery.'
136 136 if key_name.startswith(pref):
137 137 return key_name[len(pref):].replace('.', '_').lower()
138 138
139 139 def type_converter(parsed_key, value):
140 140 # cast to int
141 141 if value.isdigit():
142 142 return int(value)
143 143
144 144 # cast to bool
145 145 if value.lower() in ['true', 'false', 'True', 'False']:
146 146 return value.lower() == 'true'
147 147 return value
148 148
149 149 celery_config = {}
150 150 for k, v in settings.items():
151 151 pref = 'celery.'
152 152 if k.startswith(pref):
153 153 celery_config[key_converter(k)] = type_converter(key_converter(k), v)
154 154
155 155 # TODO:rethink if we want to support celerybeat based file config, probably NOT
156 156 # beat_config = {}
157 157 # for section in parser.sections():
158 158 # if section.startswith('celerybeat:'):
159 159 # name = section.split(':', 1)[1]
160 160 # beat_config[name] = get_beat_config(parser, section)
161 161
162 162 # final compose of settings
163 163 celery_settings = {}
164 164
165 165 if celery_config:
166 166 celery_settings.update(celery_config)
167 167 # if beat_config:
168 168 # celery_settings.update({'beat_schedule': beat_config})
169 169
170 170 return celery_settings
171 171
172 172
173 173 def not_found_view(request):
174 174 """
175 175 This creates the view which should be registered as not-found-view to
176 176 pyramid.
177 177 """
178 178
179 179 if not getattr(request, 'vcs_call', None):
180 180 # handle like regular case with our error_handler
181 181 return error_handler(HTTPNotFound(), request)
182 182
183 183 # handle not found view as a vcs call
184 184 settings = request.registry.settings
185 185 ae_client = getattr(request, 'ae_client', None)
186 186 vcs_app = VCSMiddleware(
187 187 HTTPNotFound(), request.registry, settings,
188 188 appenlight_client=ae_client)
189 189
190 190 return wsgiapp(vcs_app)(None, request)
191 191
192 192
193 193 def error_handler(exception, request):
194 194 import rhodecode
195 195 from rhodecode.lib import helpers
196 196 from rhodecode.lib.utils2 import str2bool
197 197
198 198 rhodecode_title = rhodecode.CONFIG.get('rhodecode_title') or 'RhodeCode'
199 199
200 200 base_response = HTTPInternalServerError()
201 201 # prefer original exception for the response since it may have headers set
202 202 if isinstance(exception, HTTPException):
203 203 base_response = exception
204 204 elif isinstance(exception, VCSCommunicationError):
205 205 base_response = VCSServerUnavailable()
206 206
207 207 if is_http_error(base_response):
208 208 log.exception(
209 209 'error occurred handling this request for path: %s', request.path)
210 210
211 211 error_explanation = base_response.explanation or str(base_response)
212 212 if base_response.status_code == 404:
213 213 error_explanation += " Optionally you don't have permission to access this page."
214 214 c = AttributeDict()
215 215 c.error_message = base_response.status
216 216 c.error_explanation = error_explanation
217 217 c.visual = AttributeDict()
218 218
219 219 c.visual.rhodecode_support_url = (
220 220 request.registry.settings.get('rhodecode_support_url') or
221 221 request.route_url('rhodecode_support')
222 222 )
223 223 c.redirect_time = 0
224 224 c.rhodecode_name = rhodecode_title
225 225 if not c.rhodecode_name:
226 226 c.rhodecode_name = 'Rhodecode'
227 227
228 228 c.causes = []
229 229 if is_http_error(base_response):
230 230 c.causes.append('Server is overloaded.')
231 231 c.causes.append('Server database connection is lost.')
232 232 c.causes.append('Server expected unhandled error.')
233 233
234 234 if hasattr(base_response, 'causes'):
235 235 c.causes = base_response.causes
236 236
237 237 c.messages = helpers.flash.pop_messages(request=request)
238 238
239 239 exc_info = sys.exc_info()
240 240 c.exception_id = id(exc_info)
241 241 c.show_exception_id = isinstance(base_response, VCSServerUnavailable) \
242 242 or base_response.status_code > 499
243 243 c.exception_id_url = request.route_url(
244 244 'admin_settings_exception_tracker_show', exception_id=c.exception_id)
245 245
246 246 if c.show_exception_id:
247 247 store_exception(c.exception_id, exc_info)
248 248 c.exception_debug = str2bool(rhodecode.CONFIG.get('debug'))
249 249 c.exception_config_ini = rhodecode.CONFIG.get('__file__')
250 250
251 251 response = render_to_response(
252 252 '/errors/error_document.mako', {'c': c, 'h': helpers}, request=request,
253 253 response=base_response)
254 254
255 255 statsd = request.registry.statsd
256 256 if statsd and base_response.status_code > 499:
257 257 exc_type = "{}.{}".format(exception.__class__.__module__, exception.__class__.__name__)
258 258 statsd.incr('rhodecode_exception_total',
259 259 tags=["exc_source:web",
260 260 "http_code:{}".format(base_response.status_code),
261 261 "type:{}".format(exc_type)])
262 262
263 263 return response
264 264
265 265
266 266 def includeme_first(config):
267 267 # redirect automatic browser favicon.ico requests to correct place
268 268 def favicon_redirect(context, request):
269 269 return HTTPFound(
270 270 request.static_path('rhodecode:public/images/favicon.ico'))
271 271
272 272 config.add_view(favicon_redirect, route_name='favicon')
273 273 config.add_route('favicon', '/favicon.ico')
274 274
275 275 def robots_redirect(context, request):
276 276 return HTTPFound(
277 277 request.static_path('rhodecode:public/robots.txt'))
278 278
279 279 config.add_view(robots_redirect, route_name='robots')
280 280 config.add_route('robots', '/robots.txt')
281 281
282 282 config.add_static_view(
283 283 '_static/deform', 'deform:static')
284 284 config.add_static_view(
285 285 '_static/rhodecode', path='rhodecode:public', cache_max_age=3600 * 24)
286 286
287 287
288 288 def includeme(config, auth_resources=None):
289 289 from rhodecode.lib.celerylib.loader import configure_celery
290 290 log.debug('Initializing main includeme from %s', os.path.basename(__file__))
291 291 settings = config.registry.settings
292 292 config.set_request_factory(Request)
293 293
294 294 # plugin information
295 295 config.registry.rhodecode_plugins = collections.OrderedDict()
296 296
297 297 config.add_directive(
298 298 'register_rhodecode_plugin', register_rhodecode_plugin)
299 299
300 300 config.add_directive('configure_celery', configure_celery)
301 301
302 302 if settings.get('appenlight', False):
303 303 config.include('appenlight_client.ext.pyramid_tween')
304 304
305 305 load_all = should_load_all()
306 306
307 307 # Includes which are required. The application would fail without them.
308 308 config.include('pyramid_mako')
309 309 config.include('rhodecode.lib.rc_beaker')
310 310 config.include('rhodecode.lib.rc_cache')
311 311 config.include('rhodecode.apps._base.navigation')
312 312 config.include('rhodecode.apps._base.subscribers')
313 313 config.include('rhodecode.tweens')
314 314 config.include('rhodecode.authentication')
315 315
316 316 if load_all:
317 317 ce_auth_resources = [
318 318 'rhodecode.authentication.plugins.auth_crowd',
319 319 'rhodecode.authentication.plugins.auth_headers',
320 320 'rhodecode.authentication.plugins.auth_jasig_cas',
321 321 'rhodecode.authentication.plugins.auth_ldap',
322 322 'rhodecode.authentication.plugins.auth_pam',
323 323 'rhodecode.authentication.plugins.auth_rhodecode',
324 324 'rhodecode.authentication.plugins.auth_token',
325 325 ]
326 326
327 327 # load CE authentication plugins
328 328
329 329 if auth_resources:
330 330 ce_auth_resources.extend(auth_resources)
331 331
332 332 for resource in ce_auth_resources:
333 333 config.include(resource)
334 334
335 335 # Auto discover authentication plugins and include their configuration.
336 336 if asbool(settings.get('auth_plugin.import_legacy_plugins', 'true')):
337 337 from rhodecode.authentication import discover_legacy_plugins
338 338 discover_legacy_plugins(config)
339 339
340 340 # apps
341 341 if load_all:
342 342 config.include('rhodecode.api')
343 343 config.include('rhodecode.apps._base')
344 344 config.include('rhodecode.apps.hovercards')
345 345 config.include('rhodecode.apps.ops')
346 346 config.include('rhodecode.apps.channelstream')
347 347 config.include('rhodecode.apps.file_store')
348 348 config.include('rhodecode.apps.admin')
349 349 config.include('rhodecode.apps.login')
350 350 config.include('rhodecode.apps.home')
351 351 config.include('rhodecode.apps.journal')
352 352
353 353 config.include('rhodecode.apps.repository')
354 354 config.include('rhodecode.apps.repo_group')
355 355 config.include('rhodecode.apps.user_group')
356 356 config.include('rhodecode.apps.search')
357 357 config.include('rhodecode.apps.user_profile')
358 358 config.include('rhodecode.apps.user_group_profile')
359 359 config.include('rhodecode.apps.my_account')
360 360 config.include('rhodecode.apps.gist')
361 361
362 362 config.include('rhodecode.apps.svn_support')
363 363 config.include('rhodecode.apps.ssh_support')
364 364 config.include('rhodecode.apps.debug_style')
365 365
366 366 if load_all:
367 367 config.include('rhodecode.integrations')
368 368
369 369 config.add_route('rhodecode_support', 'https://rhodecode.com/help/', static=True)
370 370 config.add_translation_dirs('rhodecode:i18n/')
371 371 settings['default_locale_name'] = settings.get('lang', 'en')
372 372
373 373 # Add subscribers.
374 374 if load_all:
375 375 config.add_subscriber(scan_repositories_if_enabled,
376 376 pyramid.events.ApplicationCreated)
377 377 config.add_subscriber(write_metadata_if_needed,
378 378 pyramid.events.ApplicationCreated)
379 379 config.add_subscriber(write_usage_data,
380 380 pyramid.events.ApplicationCreated)
381 381 config.add_subscriber(write_js_routes_if_enabled,
382 382 pyramid.events.ApplicationCreated)
383 383
384 # request custom methods
385 config.add_request_method(
386 'rhodecode.lib.partial_renderer.get_partial_renderer',
387 'get_partial_renderer')
388
389 config.add_request_method(
390 'rhodecode.lib.request_counter.get_request_counter',
391 'request_count')
392
393 384 # Set the authorization policy.
394 385 authz_policy = ACLAuthorizationPolicy()
395 386 config.set_authorization_policy(authz_policy)
396 387
397 388 # Set the default renderer for HTML templates to mako.
398 389 config.add_mako_renderer('.html')
399 390
400 391 config.add_renderer(
401 392 name='json_ext',
402 393 factory='rhodecode.lib.ext_json_renderer.pyramid_ext_json')
403 394
404 395 config.add_renderer(
405 396 name='string_html',
406 397 factory='rhodecode.lib.string_renderer.html')
407 398
408 399 # include RhodeCode plugins
409 400 includes = aslist(settings.get('rhodecode.includes', []))
410 401 for inc in includes:
411 402 config.include(inc)
412 403
413 404 # custom not found view, if our pyramid app doesn't know how to handle
414 405 # the request pass it to potential VCS handling ap
415 406 config.add_notfound_view(not_found_view)
416 407 if not settings.get('debugtoolbar.enabled', False):
417 408 # disabled debugtoolbar handle all exceptions via the error_handlers
418 409 config.add_view(error_handler, context=Exception)
419 410
420 411 # all errors including 403/404/50X
421 412 config.add_view(error_handler, context=HTTPError)
422 413
423 414
424 415 def wrap_app_in_wsgi_middlewares(pyramid_app, config):
425 416 """
426 417 Apply outer WSGI middlewares around the application.
427 418 """
428 419 registry = config.registry
429 420 settings = registry.settings
430 421
431 422 # enable https redirects based on HTTP_X_URL_SCHEME set by proxy
432 423 pyramid_app = HttpsFixup(pyramid_app, settings)
433 424
434 425 pyramid_app, _ae_client = wrap_in_appenlight_if_enabled(
435 426 pyramid_app, settings)
436 427 registry.ae_client = _ae_client
437 428
438 429 if settings['gzip_responses']:
439 430 pyramid_app = make_gzip_middleware(
440 431 pyramid_app, settings, compress_level=1)
441 432
442 433 # this should be the outer most middleware in the wsgi stack since
443 434 # middleware like Routes make database calls
444 435 def pyramid_app_with_cleanup(environ, start_response):
445 436 try:
446 437 return pyramid_app(environ, start_response)
447 438 finally:
448 439 # Dispose current database session and rollback uncommitted
449 440 # transactions.
450 441 meta.Session.remove()
451 442
452 443 # In a single threaded mode server, on non sqlite db we should have
453 444 # '0 Current Checked out connections' at the end of a request,
454 445 # if not, then something, somewhere is leaving a connection open
455 446 pool = meta.Base.metadata.bind.engine.pool
456 447 log.debug('sa pool status: %s', pool.status())
457 448 log.debug('Request processing finalized')
458 449
459 450 return pyramid_app_with_cleanup
460 451
461 452
462 453 def sanitize_settings_and_apply_defaults(global_config, settings):
463 454 """
464 455 Applies settings defaults and does all type conversion.
465 456
466 457 We would move all settings parsing and preparation into this place, so that
467 458 we have only one place left which deals with this part. The remaining parts
468 459 of the application would start to rely fully on well prepared settings.
469 460
470 461 This piece would later be split up per topic to avoid a big fat monster
471 462 function.
472 463 """
473 464
474 465 global_settings_maker = SettingsMaker(global_config)
475 466 global_settings_maker.make_setting('debug', default=False, parser='bool')
476 467 debug_enabled = asbool(global_config.get('debug'))
477 468
478 469 settings_maker = SettingsMaker(settings)
479 470
480 471 settings_maker.make_setting(
481 472 'logging.autoconfigure',
482 473 default=False,
483 474 parser='bool')
484 475
485 476 logging_conf = os.path.join(os.path.dirname(global_config.get('__file__')), 'logging.ini')
486 477 settings_maker.enable_logging(logging_conf, level='INFO' if debug_enabled else 'DEBUG')
487 478
488 479 # Default includes, possible to change as a user
489 480 pyramid_includes = settings_maker.make_setting('pyramid.includes', [], parser='list:newline')
490 481 log.debug(
491 482 "Using the following pyramid.includes: %s",
492 483 pyramid_includes)
493 484
494 485 settings_maker.make_setting('rhodecode.edition', 'Community Edition')
495 486 settings_maker.make_setting('rhodecode.edition_id', 'CE')
496 487
497 488 if 'mako.default_filters' not in settings:
498 489 # set custom default filters if we don't have it defined
499 490 settings['mako.imports'] = 'from rhodecode.lib.base import h_filter'
500 491 settings['mako.default_filters'] = 'h_filter'
501 492
502 493 if 'mako.directories' not in settings:
503 494 mako_directories = settings.setdefault('mako.directories', [
504 495 # Base templates of the original application
505 496 'rhodecode:templates',
506 497 ])
507 498 log.debug(
508 499 "Using the following Mako template directories: %s",
509 500 mako_directories)
510 501
511 502 # NOTE(marcink): fix redis requirement for schema of connection since 3.X
512 503 if 'beaker.session.type' in settings and settings['beaker.session.type'] == 'ext:redis':
513 504 raw_url = settings['beaker.session.url']
514 505 if not raw_url.startswith(('redis://', 'rediss://', 'unix://')):
515 506 settings['beaker.session.url'] = 'redis://' + raw_url
516 507
517 508 settings_maker.make_setting('__file__', global_config.get('__file__'))
518 509
519 510 # TODO: johbo: Re-think this, usually the call to config.include
520 511 # should allow to pass in a prefix.
521 512 settings_maker.make_setting('rhodecode.api.url', '/_admin/api')
522 513
523 514 # Sanitize generic settings.
524 515 settings_maker.make_setting('default_encoding', 'UTF-8', parser='list')
525 516 settings_maker.make_setting('is_test', False, parser='bool')
526 517 settings_maker.make_setting('gzip_responses', False, parser='bool')
527 518
528 519 # statsd
529 520 settings_maker.make_setting('statsd.enabled', False, parser='bool')
530 521 settings_maker.make_setting('statsd.statsd_host', 'statsd-exporter', parser='string')
531 522 settings_maker.make_setting('statsd.statsd_port', 9125, parser='int')
532 523 settings_maker.make_setting('statsd.statsd_prefix', '')
533 524 settings_maker.make_setting('statsd.statsd_ipv6', False, parser='bool')
534 525
535 526 settings_maker.make_setting('vcs.svn.compatible_version', '')
536 527 settings_maker.make_setting('vcs.hooks.protocol', 'http')
537 528 settings_maker.make_setting('vcs.hooks.host', '127.0.0.1')
538 529 settings_maker.make_setting('vcs.scm_app_implementation', 'http')
539 530 settings_maker.make_setting('vcs.server', '')
540 531 settings_maker.make_setting('vcs.server.protocol', 'http')
541 532 settings_maker.make_setting('startup.import_repos', 'false', parser='bool')
542 533 settings_maker.make_setting('vcs.hooks.direct_calls', 'false', parser='bool')
543 534 settings_maker.make_setting('vcs.server.enable', 'true', parser='bool')
544 535 settings_maker.make_setting('vcs.start_server', 'false', parser='bool')
545 536 settings_maker.make_setting('vcs.backends', 'hg, git, svn', parser='list')
546 537 settings_maker.make_setting('vcs.connection_timeout', 3600, parser='int')
547 538
548 539 settings_maker.make_setting('vcs.methods.cache', True, parser='bool')
549 540
550 541 # Support legacy values of vcs.scm_app_implementation. Legacy
551 542 # configurations may use 'rhodecode.lib.middleware.utils.scm_app_http', or
552 543 # disabled since 4.13 'vcsserver.scm_app' which is now mapped to 'http'.
553 544 scm_app_impl = settings['vcs.scm_app_implementation']
554 545 if scm_app_impl in ['rhodecode.lib.middleware.utils.scm_app_http', 'vcsserver.scm_app']:
555 546 settings['vcs.scm_app_implementation'] = 'http'
556 547
557 548 settings_maker.make_setting('appenlight', False, parser='bool')
558 549
559 550 temp_store = tempfile.gettempdir()
560 551 default_cache_dir = os.path.join(temp_store, 'rc_cache')
561 552
562 553 # save default, cache dir, and use it for all backends later.
563 554 default_cache_dir = settings_maker.make_setting(
564 555 'cache_dir',
565 556 default=default_cache_dir, default_when_empty=True,
566 557 parser='dir:ensured')
567 558
568 559 # exception store cache
569 560 settings_maker.make_setting(
570 561 'exception_tracker.store_path',
571 562 default=os.path.join(default_cache_dir, 'exc_store'), default_when_empty=True,
572 563 parser='dir:ensured'
573 564 )
574 565
575 566 settings_maker.make_setting(
576 567 'celerybeat-schedule.path',
577 568 default=os.path.join(default_cache_dir, 'celerybeat_schedule', 'celerybeat-schedule.db'), default_when_empty=True,
578 569 parser='file:ensured'
579 570 )
580 571
581 572 settings_maker.make_setting('exception_tracker.send_email', False, parser='bool')
582 573 settings_maker.make_setting('exception_tracker.email_prefix', '[RHODECODE ERROR]', default_when_empty=True)
583 574
584 575 # cache_general
585 576 settings_maker.make_setting('rc_cache.cache_general.backend', 'dogpile.cache.rc.file_namespace')
586 577 settings_maker.make_setting('rc_cache.cache_general.expiration_time', 60 * 60 * 12, parser='int')
587 578 settings_maker.make_setting('rc_cache.cache_general.arguments.filename', os.path.join(default_cache_dir, 'rhodecode_cache_general.db'))
588 579
589 580 # cache_perms
590 581 settings_maker.make_setting('rc_cache.cache_perms.backend', 'dogpile.cache.rc.file_namespace')
591 582 settings_maker.make_setting('rc_cache.cache_perms.expiration_time', 60 * 60, parser='int')
592 583 settings_maker.make_setting('rc_cache.cache_perms.arguments.filename', os.path.join(default_cache_dir, 'rhodecode_cache_perms.db'))
593 584
594 585 # cache_repo
595 586 settings_maker.make_setting('rc_cache.cache_repo.backend', 'dogpile.cache.rc.file_namespace')
596 587 settings_maker.make_setting('rc_cache.cache_repo.expiration_time', 60 * 60 * 24 * 30, parser='int')
597 588 settings_maker.make_setting('rc_cache.cache_repo.arguments.filename', os.path.join(default_cache_dir, 'rhodecode_cache_repo.db'))
598 589
599 590 # cache_license
600 591 settings_maker.make_setting('rc_cache.cache_license.backend', 'dogpile.cache.rc.file_namespace')
601 592 settings_maker.make_setting('rc_cache.cache_license.expiration_time', 60 * 5, parser='int')
602 593 settings_maker.make_setting('rc_cache.cache_license.arguments.filename', os.path.join(default_cache_dir, 'rhodecode_cache_license.db'))
603 594
604 595 # cache_repo_longterm memory, 96H
605 596 settings_maker.make_setting('rc_cache.cache_repo_longterm.backend', 'dogpile.cache.rc.memory_lru')
606 597 settings_maker.make_setting('rc_cache.cache_repo_longterm.expiration_time', 345600, parser='int')
607 598 settings_maker.make_setting('rc_cache.cache_repo_longterm.max_size', 10000, parser='int')
608 599
609 600 # sql_cache_short
610 601 settings_maker.make_setting('rc_cache.sql_cache_short.backend', 'dogpile.cache.rc.memory_lru')
611 602 settings_maker.make_setting('rc_cache.sql_cache_short.expiration_time', 30, parser='int')
612 603 settings_maker.make_setting('rc_cache.sql_cache_short.max_size', 10000, parser='int')
613 604
614 605 settings_maker.env_expand()
615 606
616 607 # configure instance id
617 608 config_utils.set_instance_id(settings)
618 609
619 610 return settings
@@ -1,635 +1,611 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2010-2020 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
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 Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 """
22 22 The base Controller API
23 23 Provides the BaseController class for subclassing. And usage in different
24 24 controllers
25 25 """
26 26
27 27 import logging
28 28 import socket
29 29
30 30 import markupsafe
31 31 import ipaddress
32 32
33 33 from paste.auth.basic import AuthBasicAuthenticator
34 34 from paste.httpexceptions import HTTPUnauthorized, HTTPForbidden, get_exception
35 35 from paste.httpheaders import WWW_AUTHENTICATE, AUTHORIZATION
36 36
37 37 import rhodecode
38 from rhodecode.apps._base import TemplateArgs
39 38 from rhodecode.authentication.base import VCS_TYPE
40 39 from rhodecode.lib import auth, utils2
41 40 from rhodecode.lib import helpers as h
42 41 from rhodecode.lib.auth import AuthUser, CookieStoreWrapper
43 42 from rhodecode.lib.exceptions import UserCreationError
44 43 from rhodecode.lib.utils import (password_changed, get_enabled_hook_classes)
45 44 from rhodecode.lib.utils2 import (
46 45 str2bool, safe_unicode, AttributeDict, safe_int, sha1, aslist, safe_str)
47 46 from rhodecode.model.db import Repository, User, ChangesetComment, UserBookmark
48 47 from rhodecode.model.notification import NotificationModel
49 48 from rhodecode.model.settings import VcsSettingsModel, SettingsModel
50 49
51 50 log = logging.getLogger(__name__)
52 51
53 52
54 53 def _filter_proxy(ip):
55 54 """
56 55 Passed in IP addresses in HEADERS can be in a special format of multiple
57 56 ips. Those comma separated IPs are passed from various proxies in the
58 57 chain of request processing. The left-most being the original client.
59 58 We only care about the first IP which came from the org. client.
60 59
61 60 :param ip: ip string from headers
62 61 """
63 62 if ',' in ip:
64 63 _ips = ip.split(',')
65 64 _first_ip = _ips[0].strip()
66 65 log.debug('Got multiple IPs %s, using %s', ','.join(_ips), _first_ip)
67 66 return _first_ip
68 67 return ip
69 68
70 69
71 70 def _filter_port(ip):
72 71 """
73 72 Removes a port from ip, there are 4 main cases to handle here.
74 73 - ipv4 eg. 127.0.0.1
75 74 - ipv6 eg. ::1
76 75 - ipv4+port eg. 127.0.0.1:8080
77 76 - ipv6+port eg. [::1]:8080
78 77
79 78 :param ip:
80 79 """
81 80 def is_ipv6(ip_addr):
82 81 if hasattr(socket, 'inet_pton'):
83 82 try:
84 83 socket.inet_pton(socket.AF_INET6, ip_addr)
85 84 except socket.error:
86 85 return False
87 86 else:
88 87 # fallback to ipaddress
89 88 try:
90 89 ipaddress.IPv6Address(safe_unicode(ip_addr))
91 90 except Exception:
92 91 return False
93 92 return True
94 93
95 94 if ':' not in ip: # must be ipv4 pure ip
96 95 return ip
97 96
98 97 if '[' in ip and ']' in ip: # ipv6 with port
99 98 return ip.split(']')[0][1:].lower()
100 99
101 100 # must be ipv6 or ipv4 with port
102 101 if is_ipv6(ip):
103 102 return ip
104 103 else:
105 104 ip, _port = ip.split(':')[:2] # means ipv4+port
106 105 return ip
107 106
108 107
109 108 def get_ip_addr(environ):
110 109 proxy_key = 'HTTP_X_REAL_IP'
111 110 proxy_key2 = 'HTTP_X_FORWARDED_FOR'
112 111 def_key = 'REMOTE_ADDR'
113 112 _filters = lambda x: _filter_port(_filter_proxy(x))
114 113
115 114 ip = environ.get(proxy_key)
116 115 if ip:
117 116 return _filters(ip)
118 117
119 118 ip = environ.get(proxy_key2)
120 119 if ip:
121 120 return _filters(ip)
122 121
123 122 ip = environ.get(def_key, '0.0.0.0')
124 123 return _filters(ip)
125 124
126 125
127 126 def get_server_ip_addr(environ, log_errors=True):
128 127 hostname = environ.get('SERVER_NAME')
129 128 try:
130 129 return socket.gethostbyname(hostname)
131 130 except Exception as e:
132 131 if log_errors:
133 132 # in some cases this lookup is not possible, and we don't want to
134 133 # make it an exception in logs
135 134 log.exception('Could not retrieve server ip address: %s', e)
136 135 return hostname
137 136
138 137
139 138 def get_server_port(environ):
140 139 return environ.get('SERVER_PORT')
141 140
142 141
143 142 def get_access_path(environ):
144 143 path = environ.get('PATH_INFO')
145 144 org_req = environ.get('pylons.original_request')
146 145 if org_req:
147 146 path = org_req.environ.get('PATH_INFO')
148 147 return path
149 148
150 149
151 150 def get_user_agent(environ):
152 151 return environ.get('HTTP_USER_AGENT')
153 152
154 153
155 154 def vcs_operation_context(
156 155 environ, repo_name, username, action, scm, check_locking=True,
157 156 is_shadow_repo=False, check_branch_perms=False, detect_force_push=False):
158 157 """
159 158 Generate the context for a vcs operation, e.g. push or pull.
160 159
161 160 This context is passed over the layers so that hooks triggered by the
162 161 vcs operation know details like the user, the user's IP address etc.
163 162
164 163 :param check_locking: Allows to switch of the computation of the locking
165 164 data. This serves mainly the need of the simplevcs middleware to be
166 165 able to disable this for certain operations.
167 166
168 167 """
169 168 # Tri-state value: False: unlock, None: nothing, True: lock
170 169 make_lock = None
171 170 locked_by = [None, None, None]
172 171 is_anonymous = username == User.DEFAULT_USER
173 172 user = User.get_by_username(username)
174 173 if not is_anonymous and check_locking:
175 174 log.debug('Checking locking on repository "%s"', repo_name)
176 175 repo = Repository.get_by_repo_name(repo_name)
177 176 make_lock, __, locked_by = repo.get_locking_state(
178 177 action, user.user_id)
179 178 user_id = user.user_id
180 179 settings_model = VcsSettingsModel(repo=repo_name)
181 180 ui_settings = settings_model.get_ui_settings()
182 181
183 182 # NOTE(marcink): This should be also in sync with
184 183 # rhodecode/apps/ssh_support/lib/backends/base.py:update_environment scm_data
185 184 store = [x for x in ui_settings if x.key == '/']
186 185 repo_store = ''
187 186 if store:
188 187 repo_store = store[0].value
189 188
190 189 scm_data = {
191 190 'ip': get_ip_addr(environ),
192 191 'username': username,
193 192 'user_id': user_id,
194 193 'action': action,
195 194 'repository': repo_name,
196 195 'scm': scm,
197 196 'config': rhodecode.CONFIG['__file__'],
198 197 'repo_store': repo_store,
199 198 'make_lock': make_lock,
200 199 'locked_by': locked_by,
201 200 'server_url': utils2.get_server_url(environ),
202 201 'user_agent': get_user_agent(environ),
203 202 'hooks': get_enabled_hook_classes(ui_settings),
204 203 'is_shadow_repo': is_shadow_repo,
205 204 'detect_force_push': detect_force_push,
206 205 'check_branch_perms': check_branch_perms,
207 206 }
208 207 return scm_data
209 208
210 209
211 210 class BasicAuth(AuthBasicAuthenticator):
212 211
213 212 def __init__(self, realm, authfunc, registry, auth_http_code=None,
214 213 initial_call_detection=False, acl_repo_name=None, rc_realm=''):
215 214 self.realm = realm
216 215 self.rc_realm = rc_realm
217 216 self.initial_call = initial_call_detection
218 217 self.authfunc = authfunc
219 218 self.registry = registry
220 219 self.acl_repo_name = acl_repo_name
221 220 self._rc_auth_http_code = auth_http_code
222 221
223 222 def _get_response_from_code(self, http_code):
224 223 try:
225 224 return get_exception(safe_int(http_code))
226 225 except Exception:
227 226 log.exception('Failed to fetch response for code %s', http_code)
228 227 return HTTPForbidden
229 228
230 229 def get_rc_realm(self):
231 230 return safe_str(self.rc_realm)
232 231
233 232 def build_authentication(self):
234 233 head = WWW_AUTHENTICATE.tuples('Basic realm="%s"' % self.realm)
235 234 if self._rc_auth_http_code and not self.initial_call:
236 235 # return alternative HTTP code if alternative http return code
237 236 # is specified in RhodeCode config, but ONLY if it's not the
238 237 # FIRST call
239 238 custom_response_klass = self._get_response_from_code(
240 239 self._rc_auth_http_code)
241 240 return custom_response_klass(headers=head)
242 241 return HTTPUnauthorized(headers=head)
243 242
244 243 def authenticate(self, environ):
245 244 authorization = AUTHORIZATION(environ)
246 245 if not authorization:
247 246 return self.build_authentication()
248 247 (authmeth, auth) = authorization.split(' ', 1)
249 248 if 'basic' != authmeth.lower():
250 249 return self.build_authentication()
251 250 auth = auth.strip().decode('base64')
252 251 _parts = auth.split(':', 1)
253 252 if len(_parts) == 2:
254 253 username, password = _parts
255 254 auth_data = self.authfunc(
256 255 username, password, environ, VCS_TYPE,
257 256 registry=self.registry, acl_repo_name=self.acl_repo_name)
258 257 if auth_data:
259 258 return {'username': username, 'auth_data': auth_data}
260 259 if username and password:
261 260 # we mark that we actually executed authentication once, at
262 261 # that point we can use the alternative auth code
263 262 self.initial_call = False
264 263
265 264 return self.build_authentication()
266 265
267 266 __call__ = authenticate
268 267
269 268
270 269 def calculate_version_hash(config):
271 270 return sha1(
272 271 config.get('beaker.session.secret', '') +
273 272 rhodecode.__version__)[:8]
274 273
275 274
276 275 def get_current_lang(request):
277 276 # NOTE(marcink): remove after pyramid move
278 277 try:
279 278 return translation.get_lang()[0]
280 279 except:
281 280 pass
282 281
283 282 return getattr(request, '_LOCALE_', request.locale_name)
284 283
285 284
286 285 def attach_context_attributes(context, request, user_id=None, is_api=None):
287 286 """
288 287 Attach variables into template context called `c`.
289 288 """
290 289 config = request.registry.settings
291 290
292 291 rc_config = SettingsModel().get_all_settings(cache=True, from_request=False)
293 292 context.rc_config = rc_config
294 293 context.rhodecode_version = rhodecode.__version__
295 294 context.rhodecode_edition = config.get('rhodecode.edition')
296 295 context.rhodecode_edition_id = config.get('rhodecode.edition_id')
297 296 # unique secret + version does not leak the version but keep consistency
298 297 context.rhodecode_version_hash = calculate_version_hash(config)
299 298
300 299 # Default language set for the incoming request
301 300 context.language = get_current_lang(request)
302 301
303 302 # Visual options
304 303 context.visual = AttributeDict({})
305 304
306 305 # DB stored Visual Items
307 306 context.visual.show_public_icon = str2bool(
308 307 rc_config.get('rhodecode_show_public_icon'))
309 308 context.visual.show_private_icon = str2bool(
310 309 rc_config.get('rhodecode_show_private_icon'))
311 310 context.visual.stylify_metatags = str2bool(
312 311 rc_config.get('rhodecode_stylify_metatags'))
313 312 context.visual.dashboard_items = safe_int(
314 313 rc_config.get('rhodecode_dashboard_items', 100))
315 314 context.visual.admin_grid_items = safe_int(
316 315 rc_config.get('rhodecode_admin_grid_items', 100))
317 316 context.visual.show_revision_number = str2bool(
318 317 rc_config.get('rhodecode_show_revision_number', True))
319 318 context.visual.show_sha_length = safe_int(
320 319 rc_config.get('rhodecode_show_sha_length', 100))
321 320 context.visual.repository_fields = str2bool(
322 321 rc_config.get('rhodecode_repository_fields'))
323 322 context.visual.show_version = str2bool(
324 323 rc_config.get('rhodecode_show_version'))
325 324 context.visual.use_gravatar = str2bool(
326 325 rc_config.get('rhodecode_use_gravatar'))
327 326 context.visual.gravatar_url = rc_config.get('rhodecode_gravatar_url')
328 327 context.visual.default_renderer = rc_config.get(
329 328 'rhodecode_markup_renderer', 'rst')
330 329 context.visual.comment_types = ChangesetComment.COMMENT_TYPES
331 330 context.visual.rhodecode_support_url = \
332 331 rc_config.get('rhodecode_support_url') or h.route_url('rhodecode_support')
333 332
334 333 context.visual.affected_files_cut_off = 60
335 334
336 335 context.pre_code = rc_config.get('rhodecode_pre_code')
337 336 context.post_code = rc_config.get('rhodecode_post_code')
338 337 context.rhodecode_name = rc_config.get('rhodecode_title')
339 338 context.default_encodings = aslist(config.get('default_encoding'), sep=',')
340 339 # if we have specified default_encoding in the request, it has more
341 340 # priority
342 341 if request.GET.get('default_encoding'):
343 342 context.default_encodings.insert(0, request.GET.get('default_encoding'))
344 343 context.clone_uri_tmpl = rc_config.get('rhodecode_clone_uri_tmpl')
345 344 context.clone_uri_id_tmpl = rc_config.get('rhodecode_clone_uri_id_tmpl')
346 345 context.clone_uri_ssh_tmpl = rc_config.get('rhodecode_clone_uri_ssh_tmpl')
347 346
348 347 # INI stored
349 348 context.labs_active = str2bool(
350 349 config.get('labs_settings_active', 'false'))
351 350 context.ssh_enabled = str2bool(
352 351 config.get('ssh.generate_authorized_keyfile', 'false'))
353 352 context.ssh_key_generator_enabled = str2bool(
354 353 config.get('ssh.enable_ui_key_generator', 'true'))
355 354
356 355 context.visual.allow_repo_location_change = str2bool(
357 356 config.get('allow_repo_location_change', True))
358 357 context.visual.allow_custom_hooks_settings = str2bool(
359 358 config.get('allow_custom_hooks_settings', True))
360 359 context.debug_style = str2bool(config.get('debug_style', False))
361 360
362 361 context.rhodecode_instanceid = config.get('instance_id')
363 362
364 363 context.visual.cut_off_limit_diff = safe_int(
365 364 config.get('cut_off_limit_diff'))
366 365 context.visual.cut_off_limit_file = safe_int(
367 366 config.get('cut_off_limit_file'))
368 367
369 368 context.license = AttributeDict({})
370 369 context.license.hide_license_info = str2bool(
371 370 config.get('license.hide_license_info', False))
372 371
373 372 # AppEnlight
374 373 context.appenlight_enabled = config.get('appenlight', False)
375 374 context.appenlight_api_public_key = config.get(
376 375 'appenlight.api_public_key', '')
377 376 context.appenlight_server_url = config.get('appenlight.server_url', '')
378 377
379 378 diffmode = {
380 379 "unified": "unified",
381 380 "sideside": "sideside"
382 381 }.get(request.GET.get('diffmode'))
383 382
384 383 if is_api is not None:
385 384 is_api = hasattr(request, 'rpc_user')
386 385 session_attrs = {
387 386 # defaults
388 387 "clone_url_format": "http",
389 388 "diffmode": "sideside",
390 389 "license_fingerprint": request.session.get('license_fingerprint')
391 390 }
392 391
393 392 if not is_api:
394 393 # don't access pyramid session for API calls
395 394 if diffmode and diffmode != request.session.get('rc_user_session_attr.diffmode'):
396 395 request.session['rc_user_session_attr.diffmode'] = diffmode
397 396
398 397 # session settings per user
399 398
400 399 for k, v in request.session.items():
401 400 pref = 'rc_user_session_attr.'
402 401 if k and k.startswith(pref):
403 402 k = k[len(pref):]
404 403 session_attrs[k] = v
405 404
406 405 context.user_session_attrs = session_attrs
407 406
408 407 # JS template context
409 408 context.template_context = {
410 409 'repo_name': None,
411 410 'repo_type': None,
412 411 'repo_landing_commit': None,
413 412 'rhodecode_user': {
414 413 'username': None,
415 414 'email': None,
416 415 'notification_status': False
417 416 },
418 417 'session_attrs': session_attrs,
419 418 'visual': {
420 419 'default_renderer': None
421 420 },
422 421 'commit_data': {
423 422 'commit_id': None
424 423 },
425 424 'pull_request_data': {'pull_request_id': None},
426 425 'timeago': {
427 426 'refresh_time': 120 * 1000,
428 427 'cutoff_limit': 1000 * 60 * 60 * 24 * 7
429 428 },
430 429 'pyramid_dispatch': {
431 430
432 431 },
433 432 'extra': {'plugins': {}}
434 433 }
435 434 # END CONFIG VARS
436 435 if is_api:
437 436 csrf_token = None
438 437 else:
439 438 csrf_token = auth.get_csrf_token(session=request.session)
440 439
441 440 context.csrf_token = csrf_token
442 441 context.backends = rhodecode.BACKENDS.keys()
443 442
444 443 unread_count = 0
445 444 user_bookmark_list = []
446 445 if user_id:
447 446 unread_count = NotificationModel().get_unread_cnt_for_user(user_id)
448 447 user_bookmark_list = UserBookmark.get_bookmarks_for_user(user_id)
449 448 context.unread_notifications = unread_count
450 449 context.bookmark_items = user_bookmark_list
451 450
452 451 # web case
453 452 if hasattr(request, 'user'):
454 453 context.auth_user = request.user
455 454 context.rhodecode_user = request.user
456 455
457 456 # api case
458 457 if hasattr(request, 'rpc_user'):
459 458 context.auth_user = request.rpc_user
460 459 context.rhodecode_user = request.rpc_user
461 460
462 461 # attach the whole call context to the request
463 # use set_call_context method if request has it
464 # sometimes in Celery context requests is "different"
465 if hasattr(request, 'set_call_context'):
466 462 request.set_call_context(context)
467 else:
468 request.call_context = context
469 463
470 464
471 465 def get_auth_user(request):
472 466 environ = request.environ
473 467 session = request.session
474 468
475 469 ip_addr = get_ip_addr(environ)
476 470
477 471 # make sure that we update permissions each time we call controller
478 472 _auth_token = (
479 473 # ?auth_token=XXX
480 474 request.GET.get('auth_token', '')
481 475 # ?api_key=XXX !LEGACY
482 476 or request.GET.get('api_key', '')
483 477 # or headers....
484 478 or request.headers.get('X-Rc-Auth-Token', '')
485 479 )
486 480 if not _auth_token and request.matchdict:
487 481 url_auth_token = request.matchdict.get('_auth_token')
488 482 _auth_token = url_auth_token
489 483 if _auth_token:
490 484 log.debug('Using URL extracted auth token `...%s`', _auth_token[-4:])
491 485
492 486 if _auth_token:
493 487 # when using API_KEY we assume user exists, and
494 488 # doesn't need auth based on cookies.
495 489 auth_user = AuthUser(api_key=_auth_token, ip_addr=ip_addr)
496 490 authenticated = False
497 491 else:
498 492 cookie_store = CookieStoreWrapper(session.get('rhodecode_user'))
499 493 try:
500 494 auth_user = AuthUser(user_id=cookie_store.get('user_id', None),
501 495 ip_addr=ip_addr)
502 496 except UserCreationError as e:
503 497 h.flash(e, 'error')
504 498 # container auth or other auth functions that create users
505 499 # on the fly can throw this exception signaling that there's
506 500 # issue with user creation, explanation should be provided
507 501 # in Exception itself. We then create a simple blank
508 502 # AuthUser
509 503 auth_user = AuthUser(ip_addr=ip_addr)
510 504
511 505 # in case someone changes a password for user it triggers session
512 506 # flush and forces a re-login
513 507 if password_changed(auth_user, session):
514 508 session.invalidate()
515 509 cookie_store = CookieStoreWrapper(session.get('rhodecode_user'))
516 510 auth_user = AuthUser(ip_addr=ip_addr)
517 511
518 512 authenticated = cookie_store.get('is_authenticated')
519 513
520 514 if not auth_user.is_authenticated and auth_user.is_user_object:
521 515 # user is not authenticated and not empty
522 516 auth_user.set_authenticated(authenticated)
523 517
524 518 return auth_user, _auth_token
525 519
526 520
527 521 def h_filter(s):
528 522 """
529 523 Custom filter for Mako templates. Mako by standard uses `markupsafe.escape`
530 524 we wrap this with additional functionality that converts None to empty
531 525 strings
532 526 """
533 527 if s is None:
534 528 return markupsafe.Markup()
535 529 return markupsafe.escape(s)
536 530
537 531
538 532 def add_events_routes(config):
539 533 """
540 534 Adds routing that can be used in events. Because some events are triggered
541 535 outside of pyramid context, we need to bootstrap request with some
542 536 routing registered
543 537 """
544 538
545 539 from rhodecode.apps._base import ADMIN_PREFIX
546 540
547 541 config.add_route(name='home', pattern='/')
548 542 config.add_route(name='main_page_repos_data', pattern='/_home_repos')
549 543 config.add_route(name='main_page_repo_groups_data', pattern='/_home_repo_groups')
550 544
551 545 config.add_route(name='login', pattern=ADMIN_PREFIX + '/login')
552 546 config.add_route(name='logout', pattern=ADMIN_PREFIX + '/logout')
553 547 config.add_route(name='repo_summary', pattern='/{repo_name}')
554 548 config.add_route(name='repo_summary_explicit', pattern='/{repo_name}/summary')
555 549 config.add_route(name='repo_group_home', pattern='/{repo_group_name}')
556 550
557 551 config.add_route(name='pullrequest_show',
558 552 pattern='/{repo_name}/pull-request/{pull_request_id}')
559 553 config.add_route(name='pull_requests_global',
560 554 pattern='/pull-request/{pull_request_id}')
561 555
562 556 config.add_route(name='repo_commit',
563 557 pattern='/{repo_name}/changeset/{commit_id}')
564 558 config.add_route(name='repo_files',
565 559 pattern='/{repo_name}/files/{commit_id}/{f_path}')
566 560
567 561 config.add_route(name='hovercard_user',
568 562 pattern='/_hovercard/user/{user_id}')
569 563
570 564 config.add_route(name='hovercard_user_group',
571 565 pattern='/_hovercard/user_group/{user_group_id}')
572 566
573 567 config.add_route(name='hovercard_pull_request',
574 568 pattern='/_hovercard/pull_request/{pull_request_id}')
575 569
576 570 config.add_route(name='hovercard_repo_commit',
577 571 pattern='/_hovercard/commit/{repo_name}/{commit_id}')
578 572
579 573
580 def bootstrap_config(request):
574 def bootstrap_config(request, registry_name='RcTestRegistry'):
581 575 import pyramid.testing
582 registry = pyramid.testing.Registry('RcTestRegistry')
576 registry = pyramid.testing.Registry(registry_name)
583 577
584 578 config = pyramid.testing.setUp(registry=registry, request=request)
585 579
586 580 # allow pyramid lookup in testing
587 581 config.include('pyramid_mako')
588 582 config.include('rhodecode.lib.rc_beaker')
589 583 config.include('rhodecode.lib.rc_cache')
590 584
591 585 add_events_routes(config)
592 586
593 587 return config
594 588
595 589
596 590 def bootstrap_request(**kwargs):
597 import pyramid.testing
591 """
592 Returns a thin version of Request Object that is used in non-web context like testing/celery
593 """
598 594
599 class TestRequest(pyramid.testing.DummyRequest):
595 import pyramid.testing
596 from rhodecode.lib.request import ThinRequest as _ThinRequest
597
598 class ThinRequest(_ThinRequest):
600 599 application_url = kwargs.pop('application_url', 'http://example.com')
601 600 host = kwargs.pop('host', 'example.com:80')
602 601 domain = kwargs.pop('domain', 'example.com')
603 602
604 def translate(self, msg):
605 return msg
606
607 def plularize(self, singular, plural, n):
608 return singular
609
610 def get_partial_renderer(self, tmpl_name):
611
612 from rhodecode.lib.partial_renderer import get_partial_renderer
613 return get_partial_renderer(request=self, tmpl_name=tmpl_name)
614
615 _call_context = TemplateArgs()
616 _call_context.visual = TemplateArgs()
617 _call_context.visual.show_sha_length = 12
618 _call_context.visual.show_revision_number = True
619
620 @property
621 def call_context(self):
622 return self._call_context
623
624 def set_call_context(self, new_context):
625 self._call_context = new_context
626
627 class TestDummySession(pyramid.testing.DummySession):
603 class ThinSession(pyramid.testing.DummySession):
628 604 def save(*arg, **kw):
629 605 pass
630 606
631 request = TestRequest(**kwargs)
632 request.session = TestDummySession()
607 request = ThinRequest(**kwargs)
608 request.session = ThinSession()
633 609
634 610 return request
635 611
@@ -1,329 +1,327 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2010-2020 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
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 Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20 """
21 21 Celery loader, run with::
22 22
23 23 celery worker \
24 24 --task-events \
25 25 --beat \
26 26 --autoscale=20,2 \
27 27 --max-tasks-per-child 1 \
28 28 --app rhodecode.lib.celerylib.loader \
29 29 --scheduler rhodecode.lib.celerylib.scheduler.RcScheduler \
30 30 --loglevel DEBUG --ini=.dev/dev.ini
31 31 """
32 32 import os
33 33 import logging
34 34 import importlib
35 35
36 36 from celery import Celery
37 37 from celery import signals
38 38 from celery import Task
39 39 from celery import exceptions # pragma: no cover
40 40 from kombu.serialization import register
41 41 from pyramid.threadlocal import get_current_request
42 42
43 43 import rhodecode
44 44
45 45 from rhodecode.lib.auth import AuthUser
46 46 from rhodecode.lib.celerylib.utils import parse_ini_vars, ping_db
47 47 from rhodecode.lib.ext_json import json
48 from rhodecode.lib.pyramid_utils import bootstrap, setup_logging, prepare_request
48 from rhodecode.lib.pyramid_utils import bootstrap, setup_logging
49 49 from rhodecode.lib.utils2 import str2bool
50 50 from rhodecode.model import meta
51 51
52 52
53 53 register('json_ext', json.dumps, json.loads,
54 54 content_type='application/x-json-ext',
55 55 content_encoding='utf-8')
56 56
57 57 log = logging.getLogger('celery.rhodecode.loader')
58 58
59 59
60 60 def add_preload_arguments(parser):
61 61 parser.add_argument(
62 62 '--ini', default=None,
63 63 help='Path to ini configuration file.'
64 64 )
65 65 parser.add_argument(
66 66 '--ini-var', default=None,
67 67 help='Comma separated list of key=value to pass to ini.'
68 68 )
69 69
70 70
71 71 def get_logger(obj):
72 72 custom_log = logging.getLogger(
73 73 'rhodecode.task.{}'.format(obj.__class__.__name__))
74 74
75 75 if rhodecode.CELERY_ENABLED:
76 76 try:
77 77 custom_log = obj.get_logger()
78 78 except Exception:
79 79 pass
80 80
81 81 return custom_log
82 82
83 83
84 84 imports = ['rhodecode.lib.celerylib.tasks']
85 85
86 86 try:
87 87 # try if we have EE tasks available
88 88 importlib.import_module('rc_ee')
89 89 imports.append('rc_ee.lib.celerylib.tasks')
90 90 except ImportError:
91 91 pass
92 92
93 93
94 94 base_celery_config = {
95 95 'result_backend': 'rpc://',
96 96 'result_expires': 60 * 60 * 24,
97 97 'result_persistent': True,
98 98 'imports': imports,
99 99 'worker_max_tasks_per_child': 100,
100 100 'accept_content': ['json_ext'],
101 101 'task_serializer': 'json_ext',
102 102 'result_serializer': 'json_ext',
103 103 'worker_hijack_root_logger': False,
104 104 'database_table_names': {
105 105 'task': 'beat_taskmeta',
106 106 'group': 'beat_groupmeta',
107 107 }
108 108 }
109 109 # init main celery app
110 110 celery_app = Celery()
111 111 celery_app.user_options['preload'].add(add_preload_arguments)
112 112 ini_file_glob = None
113 113
114 114
115 115 @signals.setup_logging.connect
116 116 def setup_logging_callback(**kwargs):
117 117 setup_logging(ini_file_glob)
118 118
119 119
120 120 @signals.user_preload_options.connect
121 121 def on_preload_parsed(options, **kwargs):
122 122 from rhodecode.config.middleware import get_celery_config
123 123
124 124 ini_location = options['ini']
125 125 ini_vars = options['ini_var']
126 126 celery_app.conf['INI_PYRAMID'] = options['ini']
127 127
128 128 if ini_location is None:
129 129 print('You must provide the paste --ini argument')
130 130 exit(-1)
131 131
132 132 options = None
133 133 if ini_vars is not None:
134 134 options = parse_ini_vars(ini_vars)
135 135
136 136 global ini_file_glob
137 137 ini_file_glob = ini_location
138 138
139 139 log.debug('Bootstrapping RhodeCode application...')
140 140
141 141 env = {}
142 142 try:
143 143 env = bootstrap(ini_location, options=options)
144 144 except Exception:
145 145 log.exception('Failed to bootstrap RhodeCode APP')
146 146
147 147 log.debug('Got Pyramid ENV: %s', env)
148 148 celery_settings = get_celery_config(env['registry'].settings)
149 149
150 150 setup_celery_app(
151 151 app=env['app'], root=env['root'], request=env['request'],
152 152 registry=env['registry'], closer=env['closer'],
153 153 celery_settings=celery_settings)
154 154
155 155 # fix the global flag even if it's disabled via .ini file because this
156 156 # is a worker code that doesn't need this to be disabled.
157 157 rhodecode.CELERY_ENABLED = True
158 158
159 159
160 160 @signals.task_prerun.connect
161 161 def task_prerun_signal(task_id, task, args, **kwargs):
162 162 ping_db()
163 163
164 164
165 165 @signals.task_success.connect
166 166 def task_success_signal(result, **kwargs):
167 167 meta.Session.commit()
168 168 closer = celery_app.conf['PYRAMID_CLOSER']
169 169 if closer:
170 170 closer()
171 171
172 172
173 173 @signals.task_retry.connect
174 174 def task_retry_signal(
175 175 request, reason, einfo, **kwargs):
176 176 meta.Session.remove()
177 177 closer = celery_app.conf['PYRAMID_CLOSER']
178 178 if closer:
179 179 closer()
180 180
181 181
182 182 @signals.task_failure.connect
183 183 def task_failure_signal(
184 184 task_id, exception, args, kwargs, traceback, einfo, **kargs):
185 185 from rhodecode.lib.exc_tracking import store_exception
186 186 from rhodecode.lib.statsd_client import StatsdClient
187 187
188 188 meta.Session.remove()
189 189
190 190 # simulate sys.exc_info()
191 191 exc_info = (einfo.type, einfo.exception, einfo.tb)
192 192 store_exception(id(exc_info), exc_info, prefix='rhodecode-celery')
193 193 statsd = StatsdClient.statsd
194 194 if statsd:
195 195 exc_type = "{}.{}".format(einfo.__class__.__module__, einfo.__class__.__name__)
196 196 statsd.incr('rhodecode_exception_total',
197 197 tags=["exc_source:celery", "type:{}".format(exc_type)])
198 198
199 199 closer = celery_app.conf['PYRAMID_CLOSER']
200 200 if closer:
201 201 closer()
202 202
203 203
204 204 @signals.task_revoked.connect
205 205 def task_revoked_signal(
206 206 request, terminated, signum, expired, **kwargs):
207 207 closer = celery_app.conf['PYRAMID_CLOSER']
208 208 if closer:
209 209 closer()
210 210
211 211
212 212 def setup_celery_app(app, root, request, registry, closer, celery_settings):
213 213 log.debug('Got custom celery conf: %s', celery_settings)
214 214 celery_config = base_celery_config
215 215 celery_config.update({
216 216 # store celerybeat scheduler db where the .ini file is
217 217 'beat_schedule_filename': registry.settings['celerybeat-schedule.path'],
218 218 })
219 219
220 220 celery_config.update(celery_settings)
221 221 celery_app.config_from_object(celery_config)
222 222
223 223 celery_app.conf.update({'PYRAMID_APP': app})
224 224 celery_app.conf.update({'PYRAMID_ROOT': root})
225 225 celery_app.conf.update({'PYRAMID_REQUEST': request})
226 226 celery_app.conf.update({'PYRAMID_REGISTRY': registry})
227 227 celery_app.conf.update({'PYRAMID_CLOSER': closer})
228 228
229 229
230 230 def configure_celery(config, celery_settings):
231 231 """
232 232 Helper that is called from our application creation logic. It gives
233 233 connection info into running webapp and allows execution of tasks from
234 234 RhodeCode itself
235 235 """
236 236 # store some globals into rhodecode
237 237 rhodecode.CELERY_ENABLED = str2bool(
238 238 config.registry.settings.get('use_celery'))
239 239 if rhodecode.CELERY_ENABLED:
240 240 log.info('Configuring celery based on `%s` settings', celery_settings)
241 241 setup_celery_app(
242 242 app=None, root=None, request=None, registry=config.registry,
243 243 closer=None, celery_settings=celery_settings)
244 244
245 245
246 246 def maybe_prepare_env(req):
247 247 environ = {}
248 248 try:
249 249 environ.update({
250 250 'PATH_INFO': req.environ['PATH_INFO'],
251 251 'SCRIPT_NAME': req.environ['SCRIPT_NAME'],
252 252 'HTTP_HOST': req.environ.get('HTTP_HOST', req.environ['SERVER_NAME']),
253 253 'SERVER_NAME': req.environ['SERVER_NAME'],
254 254 'SERVER_PORT': req.environ['SERVER_PORT'],
255 255 'wsgi.url_scheme': req.environ['wsgi.url_scheme'],
256 256 })
257 257 except Exception:
258 258 pass
259 259
260 260 return environ
261 261
262 262
263 263 class RequestContextTask(Task):
264 264 """
265 265 This is a celery task which will create a rhodecode app instance context
266 266 for the task, patch pyramid with the original request
267 267 that created the task and also add the user to the context.
268 268 """
269 269
270 270 def apply_async(self, args=None, kwargs=None, task_id=None, producer=None,
271 271 link=None, link_error=None, shadow=None, **options):
272 272 """ queue the job to run (we are in web request context here) """
273 273
274 req = get_current_request()
274 req = self.app.conf['PYRAMID_REQUEST'] or get_current_request()
275
275 276 log.debug('Running Task with class: %s. Request Class: %s',
276 277 self.__class__, req.__class__)
277 278
279 proxy_data = getattr(self.request, 'rhodecode_proxy_data', None)
280 log.debug('celery proxy data:%r', proxy_data)
281
282 user_id = None
283 ip_addr = None
284 if proxy_data:
285 user_id = proxy_data['auth_user']['user_id']
286 ip_addr = proxy_data['auth_user']['ip_addr']
287
278 288 # web case
279 289 if hasattr(req, 'user'):
280 290 ip_addr = req.user.ip_addr
281 291 user_id = req.user.user_id
282 292
283 293 # api case
284 294 elif hasattr(req, 'rpc_user'):
285 295 ip_addr = req.rpc_user.ip_addr
286 296 user_id = req.rpc_user.user_id
287 297 else:
298 if user_id and ip_addr:
299 log.debug('Using data from celery proxy user')
300
301 else:
288 302 raise Exception(
289 303 'Unable to fetch required data from request: {}. \n'
290 304 'This task is required to be executed from context of '
291 305 'request in a webapp. Task: {}'.format(
292 306 repr(req),
293 self
307 self.__class__
294 308 )
295 309 )
296 310
297 311 if req:
298 312 # we hook into kwargs since it is the only way to pass our data to
299 313 # the celery worker
300 314 environ = maybe_prepare_env(req)
301 315 options['headers'] = options.get('headers', {})
302 316 options['headers'].update({
303 317 'rhodecode_proxy_data': {
304 318 'environ': environ,
305 319 'auth_user': {
306 320 'ip_addr': ip_addr,
307 321 'user_id': user_id
308 322 },
309 323 }
310 324 })
311 325
312 326 return super(RequestContextTask, self).apply_async(
313 327 args, kwargs, task_id, producer, link, link_error, shadow, **options)
314
315 def __call__(self, *args, **kwargs):
316 """ rebuild the context and then run task on celery worker """
317
318 proxy_data = getattr(self.request, 'rhodecode_proxy_data', None)
319 if not proxy_data:
320 return super(RequestContextTask, self).__call__(*args, **kwargs)
321
322 log.debug('using celery proxy data to run task: %r', proxy_data)
323 # re-inject and register threadlocals for proper routing support
324 request = prepare_request(proxy_data['environ'])
325 request.user = AuthUser(user_id=proxy_data['auth_user']['user_id'],
326 ip_addr=proxy_data['auth_user']['ip_addr'])
327
328 return super(RequestContextTask, self).__call__(*args, **kwargs)
329
@@ -1,76 +1,54 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2016-2020 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
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 Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 import os
22 22 from pyramid.compat import configparser
23 23 from pyramid.paster import bootstrap as pyramid_bootstrap, setup_logging # pragma: no cover
24 24 from pyramid.scripting import prepare
25 25
26 26 from rhodecode.lib.request import Request
27 27
28 28
29 29 def get_config(ini_path, **kwargs):
30 30 parser = configparser.ConfigParser(**kwargs)
31 31 parser.read(ini_path)
32 32 return parser
33 33
34 34
35 35 def get_app_config(ini_path):
36 36 from paste.deploy.loadwsgi import appconfig
37 37 return appconfig('config:{}'.format(ini_path), relative_to=os.getcwd())
38 38
39 39
40 class BootstrappedRequest(Request):
41 """
42 Special version of Request Which has some available methods like in pyramid.
43 Some code (used for template rendering) requires this, and we unsure it's present.
44 """
45
46 def translate(self, msg):
47 return msg
48
49 def plularize(self, singular, plural, n):
50 return singular
51
52 def get_partial_renderer(self, tmpl_name):
53 from rhodecode.lib.partial_renderer import get_partial_renderer
54 return get_partial_renderer(request=self, tmpl_name=tmpl_name)
55
56
57 def bootstrap(config_uri, request=None, options=None, env=None):
40 def bootstrap(config_uri, options=None, env=None):
58 41 if env:
59 42 os.environ.update(env)
60 43
61 44 config = get_config(config_uri)
62 45 base_url = 'http://rhodecode.local'
63 46 try:
64 47 base_url = config.get('app:main', 'app.base_url')
65 48 except (configparser.NoSectionError, configparser.NoOptionError):
66 49 pass
67 50
68 request = request or BootstrappedRequest.blank('/', base_url=base_url)
51 request = Request.blank('/', base_url=base_url)
69 52
70 53 return pyramid_bootstrap(config_uri, request=request, options=options)
71 54
72
73 def prepare_request(environ):
74 request = Request.blank('/', environ=environ)
75 prepare(request) # set pyramid threadlocal request
76 return request
@@ -1,50 +1,109 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2017-2020 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
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 Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 from uuid import uuid4
22 import pyramid.testing
22 23 from pyramid.decorator import reify
23 24 from pyramid.request import Request as _Request
24 25 from rhodecode.translation import _ as tsf
26 from rhodecode.lib.utils2 import StrictAttributeDict
27
28
29 class TemplateArgs(StrictAttributeDict):
30 pass
25 31
26 32
27 class Request(_Request):
33 # Base Class with DummyMethods, testing / CLI scripts
34 class RequestBase(object):
28 35 _req_id_bucket = list()
36 _call_context = TemplateArgs()
37 _call_context.visual = TemplateArgs()
38 _call_context.visual.show_sha_length = 12
39 _call_context.visual.show_revision_number = True
29 40
30 41 @reify
31 42 def req_id(self):
32 43 return str(uuid4())
33 44
34 45 @property
35 46 def req_id_bucket(self):
36 47 return self._req_id_bucket
37 48
38 49 def req_id_records_init(self):
39 50 self._req_id_bucket = list()
40 51
52 def translate(self, *args, **kwargs):
53 raise NotImplementedError()
54
55 def plularize(self, *args, **kwargs):
56 raise NotImplementedError()
57
58 def get_partial_renderer(self, tmpl_name):
59 raise NotImplementedError()
60
61 @property
62 def call_context(self):
63 return self._call_context
64
65 def set_call_context(self, new_context):
66 self._call_context = new_context
67
68
69 # for thin non-web/cli etc
70 class ThinRequest(RequestBase, pyramid.testing.DummyRequest):
71
72 def translate(self, msg):
73 return msg
74
75 def plularize(self, singular, plural, n):
76 return singular
77
78 def get_partial_renderer(self, tmpl_name):
79 from rhodecode.lib.partial_renderer import get_partial_renderer
80 return get_partial_renderer(request=self, tmpl_name=tmpl_name)
81
82
83 # for real-web-based
84 class RealRequest(RequestBase, _Request):
85 def get_partial_renderer(self, tmpl_name):
86 from rhodecode.lib.partial_renderer import get_partial_renderer
87 return get_partial_renderer(request=self, tmpl_name=tmpl_name)
88
89 def request_count(self):
90 from rhodecode.lib.request_counter import get_request_counter
91 return get_request_counter()
92
41 93 def plularize(self, *args, **kwargs):
42 94 return self.localizer.pluralize(*args, **kwargs)
43 95
44 96 def translate(self, *args, **kwargs):
45 97 localizer = self.localizer
46 98
47 99 def auto_translate(*_args, **_kwargs):
48 100 return localizer.translate(tsf(*_args, **_kwargs))
49 101
50 102 return auto_translate(*args, **kwargs)
103
104
105 class Request(RealRequest):
106 """
107 This is the main request object used in web-context
108 """
109 pass
@@ -1,27 +1,27 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2017-2020 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
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 Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 counter = 0
22 22
23 23
24 def get_request_counter(request):
24 def get_request_counter():
25 25 global counter
26 26 counter += 1
27 27 return counter
General Comments 0
You need to be logged in to leave comments. Login now