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