##// END OF EJS Templates
events: make sure we propagate our dummy request with proper application_url....
marcink -
r1960:c9da1578 default
parent child Browse files
Show More
@@ -1,702 +1,706 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2010-2017 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 import pyramid.threadlocal
33 33
34 34 from paste.auth.basic import AuthBasicAuthenticator
35 35 from paste.httpexceptions import HTTPUnauthorized, HTTPForbidden, get_exception
36 36 from paste.httpheaders import WWW_AUTHENTICATE, AUTHORIZATION
37 37 from pylons import config, tmpl_context as c, request, url
38 38 from pylons.controllers import WSGIController
39 39 from pylons.controllers.util import redirect
40 40 from pylons.i18n import translation
41 41 # marcink: don't remove this import
42 42 from pylons.templating import render_mako, pylons_globals, literal, cached_template
43 43 from pylons.i18n.translation import _
44 44 from webob.exc import HTTPFound
45 45
46 46
47 47 import rhodecode
48 48 from rhodecode.authentication.base import VCS_TYPE
49 49 from rhodecode.lib import auth, utils2
50 50 from rhodecode.lib import helpers as h
51 51 from rhodecode.lib.auth import AuthUser, CookieStoreWrapper
52 52 from rhodecode.lib.exceptions import UserCreationError
53 53 from rhodecode.lib.utils import (
54 54 get_repo_slug, set_rhodecode_config, password_changed,
55 55 get_enabled_hook_classes)
56 56 from rhodecode.lib.utils2 import (
57 57 str2bool, safe_unicode, AttributeDict, safe_int, md5, aslist)
58 58 from rhodecode.lib.vcs.exceptions import RepositoryRequirementError
59 59 from rhodecode.model import meta
60 60 from rhodecode.model.db import Repository, User, ChangesetComment
61 61 from rhodecode.model.notification import NotificationModel
62 62 from rhodecode.model.scm import ScmModel
63 63 from rhodecode.model.settings import VcsSettingsModel, SettingsModel
64 64
65 65
66 66 log = logging.getLogger(__name__)
67 67
68 68
69 69 # hack to make the migration to pyramid easier
70 70 def render(template_name, extra_vars=None, cache_key=None,
71 71 cache_type=None, cache_expire=None):
72 72 """Render a template with Mako
73 73
74 74 Accepts the cache options ``cache_key``, ``cache_type``, and
75 75 ``cache_expire``.
76 76
77 77 """
78 78 # Create a render callable for the cache function
79 79 def render_template():
80 80 # Pull in extra vars if needed
81 81 globs = extra_vars or {}
82 82
83 83 # Second, get the globals
84 84 globs.update(pylons_globals())
85 85
86 86 globs['_ungettext'] = globs['ungettext']
87 87 # Grab a template reference
88 88 template = globs['app_globals'].mako_lookup.get_template(template_name)
89 89
90 90 return literal(template.render_unicode(**globs))
91 91
92 92 return cached_template(template_name, render_template, cache_key=cache_key,
93 93 cache_type=cache_type, cache_expire=cache_expire)
94 94
95 95 def _filter_proxy(ip):
96 96 """
97 97 Passed in IP addresses in HEADERS can be in a special format of multiple
98 98 ips. Those comma separated IPs are passed from various proxies in the
99 99 chain of request processing. The left-most being the original client.
100 100 We only care about the first IP which came from the org. client.
101 101
102 102 :param ip: ip string from headers
103 103 """
104 104 if ',' in ip:
105 105 _ips = ip.split(',')
106 106 _first_ip = _ips[0].strip()
107 107 log.debug('Got multiple IPs %s, using %s', ','.join(_ips), _first_ip)
108 108 return _first_ip
109 109 return ip
110 110
111 111
112 112 def _filter_port(ip):
113 113 """
114 114 Removes a port from ip, there are 4 main cases to handle here.
115 115 - ipv4 eg. 127.0.0.1
116 116 - ipv6 eg. ::1
117 117 - ipv4+port eg. 127.0.0.1:8080
118 118 - ipv6+port eg. [::1]:8080
119 119
120 120 :param ip:
121 121 """
122 122 def is_ipv6(ip_addr):
123 123 if hasattr(socket, 'inet_pton'):
124 124 try:
125 125 socket.inet_pton(socket.AF_INET6, ip_addr)
126 126 except socket.error:
127 127 return False
128 128 else:
129 129 # fallback to ipaddress
130 130 try:
131 131 ipaddress.IPv6Address(safe_unicode(ip_addr))
132 132 except Exception:
133 133 return False
134 134 return True
135 135
136 136 if ':' not in ip: # must be ipv4 pure ip
137 137 return ip
138 138
139 139 if '[' in ip and ']' in ip: # ipv6 with port
140 140 return ip.split(']')[0][1:].lower()
141 141
142 142 # must be ipv6 or ipv4 with port
143 143 if is_ipv6(ip):
144 144 return ip
145 145 else:
146 146 ip, _port = ip.split(':')[:2] # means ipv4+port
147 147 return ip
148 148
149 149
150 150 def get_ip_addr(environ):
151 151 proxy_key = 'HTTP_X_REAL_IP'
152 152 proxy_key2 = 'HTTP_X_FORWARDED_FOR'
153 153 def_key = 'REMOTE_ADDR'
154 154 _filters = lambda x: _filter_port(_filter_proxy(x))
155 155
156 156 ip = environ.get(proxy_key)
157 157 if ip:
158 158 return _filters(ip)
159 159
160 160 ip = environ.get(proxy_key2)
161 161 if ip:
162 162 return _filters(ip)
163 163
164 164 ip = environ.get(def_key, '0.0.0.0')
165 165 return _filters(ip)
166 166
167 167
168 168 def get_server_ip_addr(environ, log_errors=True):
169 169 hostname = environ.get('SERVER_NAME')
170 170 try:
171 171 return socket.gethostbyname(hostname)
172 172 except Exception as e:
173 173 if log_errors:
174 174 # in some cases this lookup is not possible, and we don't want to
175 175 # make it an exception in logs
176 176 log.exception('Could not retrieve server ip address: %s', e)
177 177 return hostname
178 178
179 179
180 180 def get_server_port(environ):
181 181 return environ.get('SERVER_PORT')
182 182
183 183
184 184 def get_access_path(environ):
185 185 path = environ.get('PATH_INFO')
186 186 org_req = environ.get('pylons.original_request')
187 187 if org_req:
188 188 path = org_req.environ.get('PATH_INFO')
189 189 return path
190 190
191 191
192 192 def get_user_agent(environ):
193 193 return environ.get('HTTP_USER_AGENT')
194 194
195 195
196 196 def vcs_operation_context(
197 197 environ, repo_name, username, action, scm, check_locking=True,
198 198 is_shadow_repo=False):
199 199 """
200 200 Generate the context for a vcs operation, e.g. push or pull.
201 201
202 202 This context is passed over the layers so that hooks triggered by the
203 203 vcs operation know details like the user, the user's IP address etc.
204 204
205 205 :param check_locking: Allows to switch of the computation of the locking
206 206 data. This serves mainly the need of the simplevcs middleware to be
207 207 able to disable this for certain operations.
208 208
209 209 """
210 210 # Tri-state value: False: unlock, None: nothing, True: lock
211 211 make_lock = None
212 212 locked_by = [None, None, None]
213 213 is_anonymous = username == User.DEFAULT_USER
214 214 if not is_anonymous and check_locking:
215 215 log.debug('Checking locking on repository "%s"', repo_name)
216 216 user = User.get_by_username(username)
217 217 repo = Repository.get_by_repo_name(repo_name)
218 218 make_lock, __, locked_by = repo.get_locking_state(
219 219 action, user.user_id)
220 220
221 221 settings_model = VcsSettingsModel(repo=repo_name)
222 222 ui_settings = settings_model.get_ui_settings()
223 223
224 224 extras = {
225 225 'ip': get_ip_addr(environ),
226 226 'username': username,
227 227 'action': action,
228 228 'repository': repo_name,
229 229 'scm': scm,
230 230 'config': rhodecode.CONFIG['__file__'],
231 231 'make_lock': make_lock,
232 232 'locked_by': locked_by,
233 233 'server_url': utils2.get_server_url(environ),
234 234 'user_agent': get_user_agent(environ),
235 235 'hooks': get_enabled_hook_classes(ui_settings),
236 236 'is_shadow_repo': is_shadow_repo,
237 237 }
238 238 return extras
239 239
240 240
241 241 class BasicAuth(AuthBasicAuthenticator):
242 242
243 243 def __init__(self, realm, authfunc, registry, auth_http_code=None,
244 244 initial_call_detection=False, acl_repo_name=None):
245 245 self.realm = realm
246 246 self.initial_call = initial_call_detection
247 247 self.authfunc = authfunc
248 248 self.registry = registry
249 249 self.acl_repo_name = acl_repo_name
250 250 self._rc_auth_http_code = auth_http_code
251 251
252 252 def _get_response_from_code(self, http_code):
253 253 try:
254 254 return get_exception(safe_int(http_code))
255 255 except Exception:
256 256 log.exception('Failed to fetch response for code %s' % http_code)
257 257 return HTTPForbidden
258 258
259 259 def build_authentication(self):
260 260 head = WWW_AUTHENTICATE.tuples('Basic realm="%s"' % self.realm)
261 261 if self._rc_auth_http_code and not self.initial_call:
262 262 # return alternative HTTP code if alternative http return code
263 263 # is specified in RhodeCode config, but ONLY if it's not the
264 264 # FIRST call
265 265 custom_response_klass = self._get_response_from_code(
266 266 self._rc_auth_http_code)
267 267 return custom_response_klass(headers=head)
268 268 return HTTPUnauthorized(headers=head)
269 269
270 270 def authenticate(self, environ):
271 271 authorization = AUTHORIZATION(environ)
272 272 if not authorization:
273 273 return self.build_authentication()
274 274 (authmeth, auth) = authorization.split(' ', 1)
275 275 if 'basic' != authmeth.lower():
276 276 return self.build_authentication()
277 277 auth = auth.strip().decode('base64')
278 278 _parts = auth.split(':', 1)
279 279 if len(_parts) == 2:
280 280 username, password = _parts
281 281 if self.authfunc(
282 282 username, password, environ, VCS_TYPE,
283 283 registry=self.registry, acl_repo_name=self.acl_repo_name):
284 284 return username
285 285 if username and password:
286 286 # we mark that we actually executed authentication once, at
287 287 # that point we can use the alternative auth code
288 288 self.initial_call = False
289 289
290 290 return self.build_authentication()
291 291
292 292 __call__ = authenticate
293 293
294 294
295 295 def calculate_version_hash():
296 296 return md5(
297 297 config.get('beaker.session.secret', '') +
298 298 rhodecode.__version__)[:8]
299 299
300 300
301 301 def get_current_lang(request):
302 302 # NOTE(marcink): remove after pyramid move
303 303 try:
304 304 return translation.get_lang()[0]
305 305 except:
306 306 pass
307 307
308 308 return getattr(request, '_LOCALE_', request.locale_name)
309 309
310 310
311 311 def attach_context_attributes(context, request, user_id):
312 312 """
313 313 Attach variables into template context called `c`, please note that
314 314 request could be pylons or pyramid request in here.
315 315 """
316 316
317 317 rc_config = SettingsModel().get_all_settings(cache=True)
318 318
319 319 context.rhodecode_version = rhodecode.__version__
320 320 context.rhodecode_edition = config.get('rhodecode.edition')
321 321 # unique secret + version does not leak the version but keep consistency
322 322 context.rhodecode_version_hash = calculate_version_hash()
323 323
324 324 # Default language set for the incoming request
325 325 context.language = get_current_lang(request)
326 326
327 327 # Visual options
328 328 context.visual = AttributeDict({})
329 329
330 330 # DB stored Visual Items
331 331 context.visual.show_public_icon = str2bool(
332 332 rc_config.get('rhodecode_show_public_icon'))
333 333 context.visual.show_private_icon = str2bool(
334 334 rc_config.get('rhodecode_show_private_icon'))
335 335 context.visual.stylify_metatags = str2bool(
336 336 rc_config.get('rhodecode_stylify_metatags'))
337 337 context.visual.dashboard_items = safe_int(
338 338 rc_config.get('rhodecode_dashboard_items', 100))
339 339 context.visual.admin_grid_items = safe_int(
340 340 rc_config.get('rhodecode_admin_grid_items', 100))
341 341 context.visual.repository_fields = str2bool(
342 342 rc_config.get('rhodecode_repository_fields'))
343 343 context.visual.show_version = str2bool(
344 344 rc_config.get('rhodecode_show_version'))
345 345 context.visual.use_gravatar = str2bool(
346 346 rc_config.get('rhodecode_use_gravatar'))
347 347 context.visual.gravatar_url = rc_config.get('rhodecode_gravatar_url')
348 348 context.visual.default_renderer = rc_config.get(
349 349 'rhodecode_markup_renderer', 'rst')
350 350 context.visual.comment_types = ChangesetComment.COMMENT_TYPES
351 351 context.visual.rhodecode_support_url = \
352 352 rc_config.get('rhodecode_support_url') or h.route_url('rhodecode_support')
353 353
354 354 context.visual.affected_files_cut_off = 60
355 355
356 356 context.pre_code = rc_config.get('rhodecode_pre_code')
357 357 context.post_code = rc_config.get('rhodecode_post_code')
358 358 context.rhodecode_name = rc_config.get('rhodecode_title')
359 359 context.default_encodings = aslist(config.get('default_encoding'), sep=',')
360 360 # if we have specified default_encoding in the request, it has more
361 361 # priority
362 362 if request.GET.get('default_encoding'):
363 363 context.default_encodings.insert(0, request.GET.get('default_encoding'))
364 364 context.clone_uri_tmpl = rc_config.get('rhodecode_clone_uri_tmpl')
365 365
366 366 # INI stored
367 367 context.labs_active = str2bool(
368 368 config.get('labs_settings_active', 'false'))
369 369 context.visual.allow_repo_location_change = str2bool(
370 370 config.get('allow_repo_location_change', True))
371 371 context.visual.allow_custom_hooks_settings = str2bool(
372 372 config.get('allow_custom_hooks_settings', True))
373 373 context.debug_style = str2bool(config.get('debug_style', False))
374 374
375 375 context.rhodecode_instanceid = config.get('instance_id')
376 376
377 377 context.visual.cut_off_limit_diff = safe_int(
378 378 config.get('cut_off_limit_diff'))
379 379 context.visual.cut_off_limit_file = safe_int(
380 380 config.get('cut_off_limit_file'))
381 381
382 382 # AppEnlight
383 383 context.appenlight_enabled = str2bool(config.get('appenlight', 'false'))
384 384 context.appenlight_api_public_key = config.get(
385 385 'appenlight.api_public_key', '')
386 386 context.appenlight_server_url = config.get('appenlight.server_url', '')
387 387
388 388 # JS template context
389 389 context.template_context = {
390 390 'repo_name': None,
391 391 'repo_type': None,
392 392 'repo_landing_commit': None,
393 393 'rhodecode_user': {
394 394 'username': None,
395 395 'email': None,
396 396 'notification_status': False
397 397 },
398 398 'visual': {
399 399 'default_renderer': None
400 400 },
401 401 'commit_data': {
402 402 'commit_id': None
403 403 },
404 404 'pull_request_data': {'pull_request_id': None},
405 405 'timeago': {
406 406 'refresh_time': 120 * 1000,
407 407 'cutoff_limit': 1000 * 60 * 60 * 24 * 7
408 408 },
409 409 'pylons_dispatch': {
410 410 # 'controller': request.environ['pylons.routes_dict']['controller'],
411 411 # 'action': request.environ['pylons.routes_dict']['action'],
412 412 },
413 413 'pyramid_dispatch': {
414 414
415 415 },
416 416 'extra': {'plugins': {}}
417 417 }
418 418 # END CONFIG VARS
419 419
420 420 # TODO: This dosn't work when called from pylons compatibility tween.
421 421 # Fix this and remove it from base controller.
422 422 # context.repo_name = get_repo_slug(request) # can be empty
423 423
424 424 diffmode = 'sideside'
425 425 if request.GET.get('diffmode'):
426 426 if request.GET['diffmode'] == 'unified':
427 427 diffmode = 'unified'
428 428 elif request.session.get('diffmode'):
429 429 diffmode = request.session['diffmode']
430 430
431 431 context.diffmode = diffmode
432 432
433 433 if request.session.get('diffmode') != diffmode:
434 434 request.session['diffmode'] = diffmode
435 435
436 436 context.csrf_token = auth.get_csrf_token(session=request.session)
437 437 context.backends = rhodecode.BACKENDS.keys()
438 438 context.backends.sort()
439 439 context.unread_notifications = NotificationModel().get_unread_cnt_for_user(user_id)
440 440
441 441 # NOTE(marcink): when migrated to pyramid we don't need to set this anymore,
442 442 # given request will ALWAYS be pyramid one
443 443 pyramid_request = pyramid.threadlocal.get_current_request()
444 444 context.pyramid_request = pyramid_request
445 445
446 446 # web case
447 447 if hasattr(pyramid_request, 'user'):
448 448 context.auth_user = pyramid_request.user
449 449 context.rhodecode_user = pyramid_request.user
450 450
451 451 # api case
452 452 if hasattr(pyramid_request, 'rpc_user'):
453 453 context.auth_user = pyramid_request.rpc_user
454 454 context.rhodecode_user = pyramid_request.rpc_user
455 455
456 456 # attach the whole call context to the request
457 457 request.call_context = context
458 458
459 459
460 460 def get_auth_user(request):
461 461 environ = request.environ
462 462 session = request.session
463 463
464 464 ip_addr = get_ip_addr(environ)
465 465 # make sure that we update permissions each time we call controller
466 466 _auth_token = (request.GET.get('auth_token', '') or
467 467 request.GET.get('api_key', ''))
468 468
469 469 if _auth_token:
470 470 # when using API_KEY we assume user exists, and
471 471 # doesn't need auth based on cookies.
472 472 auth_user = AuthUser(api_key=_auth_token, ip_addr=ip_addr)
473 473 authenticated = False
474 474 else:
475 475 cookie_store = CookieStoreWrapper(session.get('rhodecode_user'))
476 476 try:
477 477 auth_user = AuthUser(user_id=cookie_store.get('user_id', None),
478 478 ip_addr=ip_addr)
479 479 except UserCreationError as e:
480 480 h.flash(e, 'error')
481 481 # container auth or other auth functions that create users
482 482 # on the fly can throw this exception signaling that there's
483 483 # issue with user creation, explanation should be provided
484 484 # in Exception itself. We then create a simple blank
485 485 # AuthUser
486 486 auth_user = AuthUser(ip_addr=ip_addr)
487 487
488 488 if password_changed(auth_user, session):
489 489 session.invalidate()
490 490 cookie_store = CookieStoreWrapper(session.get('rhodecode_user'))
491 491 auth_user = AuthUser(ip_addr=ip_addr)
492 492
493 493 authenticated = cookie_store.get('is_authenticated')
494 494
495 495 if not auth_user.is_authenticated and auth_user.is_user_object:
496 496 # user is not authenticated and not empty
497 497 auth_user.set_authenticated(authenticated)
498 498
499 499 return auth_user
500 500
501 501
502 502 class BaseController(WSGIController):
503 503
504 504 def __before__(self):
505 505 """
506 506 __before__ is called before controller methods and after __call__
507 507 """
508 508 # on each call propagate settings calls into global settings.
509 509 set_rhodecode_config(config)
510 510 attach_context_attributes(c, request, self._rhodecode_user.user_id)
511 511
512 512 # TODO: Remove this when fixed in attach_context_attributes()
513 513 c.repo_name = get_repo_slug(request) # can be empty
514 514
515 515 self.cut_off_limit_diff = safe_int(config.get('cut_off_limit_diff'))
516 516 self.cut_off_limit_file = safe_int(config.get('cut_off_limit_file'))
517 517 self.sa = meta.Session
518 518 self.scm_model = ScmModel(self.sa)
519 519
520 520 # set user language
521 521 user_lang = getattr(c.pyramid_request, '_LOCALE_', None)
522 522 if user_lang:
523 523 translation.set_lang(user_lang)
524 524 log.debug('set language to %s for user %s',
525 525 user_lang, self._rhodecode_user)
526 526
527 527 def _dispatch_redirect(self, with_url, environ, start_response):
528 528 resp = HTTPFound(with_url)
529 529 environ['SCRIPT_NAME'] = '' # handle prefix middleware
530 530 environ['PATH_INFO'] = with_url
531 531 return resp(environ, start_response)
532 532
533 533 def __call__(self, environ, start_response):
534 534 """Invoke the Controller"""
535 535 # WSGIController.__call__ dispatches to the Controller method
536 536 # the request is routed to. This routing information is
537 537 # available in environ['pylons.routes_dict']
538 538 from rhodecode.lib import helpers as h
539 539
540 540 # Provide the Pylons context to Pyramid's debugtoolbar if it asks
541 541 if environ.get('debugtoolbar.wants_pylons_context', False):
542 542 environ['debugtoolbar.pylons_context'] = c._current_obj()
543 543
544 544 _route_name = '.'.join([environ['pylons.routes_dict']['controller'],
545 545 environ['pylons.routes_dict']['action']])
546 546
547 547 self.rc_config = SettingsModel().get_all_settings(cache=True)
548 548 self.ip_addr = get_ip_addr(environ)
549 549
550 550 # The rhodecode auth user is looked up and passed through the
551 551 # environ by the pylons compatibility tween in pyramid.
552 552 # So we can just grab it from there.
553 553 auth_user = environ['rc_auth_user']
554 554
555 555 # set globals for auth user
556 556 request.user = auth_user
557 557 self._rhodecode_user = auth_user
558 558
559 559 log.info('IP: %s User: %s accessed %s [%s]' % (
560 560 self.ip_addr, auth_user, safe_unicode(get_access_path(environ)),
561 561 _route_name)
562 562 )
563 563
564 564 user_obj = auth_user.get_instance()
565 565 if user_obj and user_obj.user_data.get('force_password_change'):
566 566 h.flash('You are required to change your password', 'warning',
567 567 ignore_duplicate=True)
568 568 return self._dispatch_redirect(
569 569 url('my_account_password'), environ, start_response)
570 570
571 571 return WSGIController.__call__(self, environ, start_response)
572 572
573 573
574 574 def h_filter(s):
575 575 """
576 576 Custom filter for Mako templates. Mako by standard uses `markupsafe.escape`
577 577 we wrap this with additional functionality that converts None to empty
578 578 strings
579 579 """
580 580 if s is None:
581 581 return markupsafe.Markup()
582 582 return markupsafe.escape(s)
583 583
584 584
585 585 def add_events_routes(config):
586 586 """
587 587 Adds routing that can be used in events. Because some events are triggered
588 588 outside of pyramid context, we need to bootstrap request with some
589 589 routing registered
590 590 """
591 591 config.add_route(name='home', pattern='/')
592 592
593 593 config.add_route(name='repo_summary', pattern='/{repo_name}')
594 594 config.add_route(name='repo_summary_explicit', pattern='/{repo_name}/summary')
595 595 config.add_route(name='repo_group_home', pattern='/{repo_group_name}')
596 596
597 597 config.add_route(name='pullrequest_show',
598 598 pattern='/{repo_name}/pull-request/{pull_request_id}')
599 599 config.add_route(name='pull_requests_global',
600 600 pattern='/pull-request/{pull_request_id}')
601 601
602 602 config.add_route(name='repo_commit',
603 603 pattern='/{repo_name}/changeset/{commit_id}')
604 604 config.add_route(name='repo_files',
605 605 pattern='/{repo_name}/files/{commit_id}/{f_path}')
606 606
607 607
608 def bootstrap_request():
608 def bootstrap_request(**kwargs):
609 609 import pyramid.testing
610 request = pyramid.testing.DummyRequest()
610 request = pyramid.testing.DummyRequest(**kwargs)
611 request.application_url = kwargs.pop('application_url', 'http://example.com')
612 request.host = kwargs.pop('host', 'example.com:80')
613 request.domain = kwargs.pop('domain', 'example.com')
614
611 615 config = pyramid.testing.setUp(request=request)
612 616 add_events_routes(config)
613 617
614 618
615 619 class BaseRepoController(BaseController):
616 620 """
617 621 Base class for controllers responsible for loading all needed data for
618 622 repository loaded items are
619 623
620 624 c.rhodecode_repo: instance of scm repository
621 625 c.rhodecode_db_repo: instance of db
622 626 c.repository_requirements_missing: shows that repository specific data
623 627 could not be displayed due to the missing requirements
624 628 c.repository_pull_requests: show number of open pull requests
625 629 """
626 630
627 631 def __before__(self):
628 632 super(BaseRepoController, self).__before__()
629 633 if c.repo_name: # extracted from routes
630 634 db_repo = Repository.get_by_repo_name(c.repo_name)
631 635 if not db_repo:
632 636 return
633 637
634 638 log.debug(
635 639 'Found repository in database %s with state `%s`',
636 640 safe_unicode(db_repo), safe_unicode(db_repo.repo_state))
637 641 route = getattr(request.environ.get('routes.route'), 'name', '')
638 642
639 643 # allow to delete repos that are somehow damages in filesystem
640 644 if route in ['delete_repo']:
641 645 return
642 646
643 647 if db_repo.repo_state in [Repository.STATE_PENDING]:
644 648 if route in ['repo_creating_home']:
645 649 return
646 650 check_url = url('repo_creating_home', repo_name=c.repo_name)
647 651 return redirect(check_url)
648 652
649 653 self.rhodecode_db_repo = db_repo
650 654
651 655 missing_requirements = False
652 656 try:
653 657 self.rhodecode_repo = self.rhodecode_db_repo.scm_instance()
654 658 except RepositoryRequirementError as e:
655 659 missing_requirements = True
656 660 self._handle_missing_requirements(e)
657 661
658 662 if self.rhodecode_repo is None and not missing_requirements:
659 663 log.error('%s this repository is present in database but it '
660 664 'cannot be created as an scm instance', c.repo_name)
661 665
662 666 h.flash(_(
663 667 "The repository at %(repo_name)s cannot be located.") %
664 668 {'repo_name': c.repo_name},
665 669 category='error', ignore_duplicate=True)
666 670 redirect(h.route_path('home'))
667 671
668 672 # update last change according to VCS data
669 673 if not missing_requirements:
670 674 commit = db_repo.get_commit(
671 675 pre_load=["author", "date", "message", "parents"])
672 676 db_repo.update_commit_cache(commit)
673 677
674 678 # Prepare context
675 679 c.rhodecode_db_repo = db_repo
676 680 c.rhodecode_repo = self.rhodecode_repo
677 681 c.repository_requirements_missing = missing_requirements
678 682
679 683 self._update_global_counters(self.scm_model, db_repo)
680 684
681 685 def _update_global_counters(self, scm_model, db_repo):
682 686 """
683 687 Base variables that are exposed to every page of repository
684 688 """
685 689 c.repository_pull_requests = scm_model.get_pull_requests(db_repo)
686 690
687 691 def _handle_missing_requirements(self, error):
688 692 self.rhodecode_repo = None
689 693 log.error(
690 694 'Requirements are missing for repository %s: %s',
691 695 c.repo_name, error.message)
692 696
693 697 summary_url = h.route_path('repo_summary', repo_name=c.repo_name)
694 698 statistics_url = url('edit_repo_statistics', repo_name=c.repo_name)
695 699 settings_update_url = url('repo', repo_name=c.repo_name)
696 700 path = request.path
697 701 should_redirect = (
698 702 path not in (summary_url, settings_update_url)
699 703 and '/settings' not in path or path == statistics_url
700 704 )
701 705 if should_redirect:
702 706 redirect(summary_url)
@@ -1,237 +1,239 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2010-2017 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 json
22 22 import logging
23 23 import traceback
24 24 import threading
25 25 from BaseHTTPServer import BaseHTTPRequestHandler
26 26 from SocketServer import TCPServer
27 27
28 28 import rhodecode
29 29 from rhodecode.model import meta
30 30 from rhodecode.lib.base import bootstrap_request
31 31 from rhodecode.lib import hooks_base
32 32 from rhodecode.lib.utils2 import AttributeDict
33 33
34 34
35 35 log = logging.getLogger(__name__)
36 36
37 37
38 38 class HooksHttpHandler(BaseHTTPRequestHandler):
39 39 def do_POST(self):
40 40 method, extras = self._read_request()
41 41 try:
42 42 result = self._call_hook(method, extras)
43 43 except Exception as e:
44 44 exc_tb = traceback.format_exc()
45 45 result = {
46 46 'exception': e.__class__.__name__,
47 47 'exception_traceback': exc_tb,
48 48 'exception_args': e.args
49 49 }
50 50 self._write_response(result)
51 51
52 52 def _read_request(self):
53 53 length = int(self.headers['Content-Length'])
54 54 body = self.rfile.read(length).decode('utf-8')
55 55 data = json.loads(body)
56 56 return data['method'], data['extras']
57 57
58 58 def _write_response(self, result):
59 59 self.send_response(200)
60 60 self.send_header("Content-type", "text/json")
61 61 self.end_headers()
62 62 self.wfile.write(json.dumps(result))
63 63
64 64 def _call_hook(self, method, extras):
65 65 hooks = Hooks()
66 66 try:
67 67 result = getattr(hooks, method)(extras)
68 68 finally:
69 69 meta.Session.remove()
70 70 return result
71 71
72 72 def log_message(self, format, *args):
73 73 """
74 74 This is an overridden method of BaseHTTPRequestHandler which logs using
75 75 logging library instead of writing directly to stderr.
76 76 """
77 77
78 78 message = format % args
79 79
80 80 # TODO: mikhail: add different log levels support
81 81 log.debug(
82 82 "%s - - [%s] %s", self.client_address[0],
83 83 self.log_date_time_string(), message)
84 84
85 85
86 86 class DummyHooksCallbackDaemon(object):
87 87 def __init__(self):
88 88 self.hooks_module = Hooks.__module__
89 89
90 90 def __enter__(self):
91 91 log.debug('Running dummy hooks callback daemon')
92 92 return self
93 93
94 94 def __exit__(self, exc_type, exc_val, exc_tb):
95 95 log.debug('Exiting dummy hooks callback daemon')
96 96
97 97
98 98 class ThreadedHookCallbackDaemon(object):
99 99
100 100 _callback_thread = None
101 101 _daemon = None
102 102 _done = False
103 103
104 104 def __init__(self):
105 105 self._prepare()
106 106
107 107 def __enter__(self):
108 108 self._run()
109 109 return self
110 110
111 111 def __exit__(self, exc_type, exc_val, exc_tb):
112 112 self._stop()
113 113
114 114 def _prepare(self):
115 115 raise NotImplementedError()
116 116
117 117 def _run(self):
118 118 raise NotImplementedError()
119 119
120 120 def _stop(self):
121 121 raise NotImplementedError()
122 122
123 123
124 124 class HttpHooksCallbackDaemon(ThreadedHookCallbackDaemon):
125 125 """
126 126 Context manager which will run a callback daemon in a background thread.
127 127 """
128 128
129 129 hooks_uri = None
130 130
131 131 IP_ADDRESS = '127.0.0.1'
132 132
133 133 # From Python docs: Polling reduces our responsiveness to a shutdown
134 134 # request and wastes cpu at all other times.
135 135 POLL_INTERVAL = 0.1
136 136
137 137 def _prepare(self):
138 138 log.debug("Preparing callback daemon and registering hook object")
139 139
140 140 self._done = False
141 141 self._daemon = TCPServer((self.IP_ADDRESS, 0), HooksHttpHandler)
142 142 _, port = self._daemon.server_address
143 143 self.hooks_uri = '{}:{}'.format(self.IP_ADDRESS, port)
144 144
145 145 log.debug("Hooks uri is: %s", self.hooks_uri)
146 146
147 147 def _run(self):
148 148 log.debug("Running event loop of callback daemon in background thread")
149 149 callback_thread = threading.Thread(
150 150 target=self._daemon.serve_forever,
151 151 kwargs={'poll_interval': self.POLL_INTERVAL})
152 152 callback_thread.daemon = True
153 153 callback_thread.start()
154 154 self._callback_thread = callback_thread
155 155
156 156 def _stop(self):
157 157 log.debug("Waiting for background thread to finish.")
158 158 self._daemon.shutdown()
159 159 self._callback_thread.join()
160 160 self._daemon = None
161 161 self._callback_thread = None
162 162
163 163
164 164 def prepare_callback_daemon(extras, protocol, use_direct_calls):
165 165 callback_daemon = None
166 166
167 167 if use_direct_calls:
168 168 callback_daemon = DummyHooksCallbackDaemon()
169 169 extras['hooks_module'] = callback_daemon.hooks_module
170 170 else:
171 171 if protocol == 'http':
172 172 callback_daemon = HttpHooksCallbackDaemon()
173 173 else:
174 174 log.error('Unsupported callback daemon protocol "%s"', protocol)
175 175 raise Exception('Unsupported callback daemon protocol.')
176 176
177 177 extras['hooks_uri'] = callback_daemon.hooks_uri
178 178 extras['hooks_protocol'] = protocol
179 179
180 180 return callback_daemon, extras
181 181
182 182
183 183 class Hooks(object):
184 184 """
185 185 Exposes the hooks for remote call backs
186 186 """
187 187
188 188 def repo_size(self, extras):
189 189 log.debug("Called repo_size of %s object", self)
190 190 return self._call_hook(hooks_base.repo_size, extras)
191 191
192 192 def pre_pull(self, extras):
193 193 log.debug("Called pre_pull of %s object", self)
194 194 return self._call_hook(hooks_base.pre_pull, extras)
195 195
196 196 def post_pull(self, extras):
197 197 log.debug("Called post_pull of %s object", self)
198 198 return self._call_hook(hooks_base.post_pull, extras)
199 199
200 200 def pre_push(self, extras):
201 201 log.debug("Called pre_push of %s object", self)
202 202 return self._call_hook(hooks_base.pre_push, extras)
203 203
204 204 def post_push(self, extras):
205 205 log.debug("Called post_push of %s object", self)
206 206 return self._call_hook(hooks_base.post_push, extras)
207 207
208 208 def _call_hook(self, hook, extras):
209 209 extras = AttributeDict(extras)
210 extras.request = bootstrap_request()
210
211 extras.request = bootstrap_request(
212 application_url=extras['server_url'])
211 213
212 214 try:
213 215 result = hook(extras)
214 216 except Exception as error:
215 217 exc_tb = traceback.format_exc()
216 218 log.exception('Exception when handling hook %s', hook)
217 219 error_args = error.args
218 220 return {
219 221 'status': 128,
220 222 'output': '',
221 223 'exception': type(error).__name__,
222 224 'exception_traceback': exc_tb,
223 225 'exception_args': error_args,
224 226 }
225 227 finally:
226 228 meta.Session.remove()
227 229
228 230 return {
229 231 'status': result.status,
230 232 'output': result.output,
231 233 }
232 234
233 235 def __enter__(self):
234 236 return self
235 237
236 238 def __exit__(self, exc_type, exc_val, exc_tb):
237 239 pass
General Comments 0
You need to be logged in to leave comments. Login now