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