##// END OF EJS Templates
Pass on RhodeCode config file down to a python-based Hg hook via the extras pseudo-config key, see https://bitbucket.org/marcinkuzminski/rhodecode/issue/558/access-to-rhodecode-config-from-a-hg
Vincent Caron -
r2857:bad89b2f beta
parent child Browse files
Show More
@@ -1,283 +1,285 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2 """
2 """
3 rhodecode.lib.middleware.simplehg
3 rhodecode.lib.middleware.simplehg
4 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
4 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
5
5
6 SimpleHG middleware for handling mercurial protocol request
6 SimpleHG middleware for handling mercurial protocol request
7 (push/clone etc.). It's implemented with basic auth function
7 (push/clone etc.). It's implemented with basic auth function
8
8
9 :created_on: Apr 28, 2010
9 :created_on: Apr 28, 2010
10 :author: marcink
10 :author: marcink
11 :copyright: (C) 2010-2012 Marcin Kuzminski <marcin@python-works.com>
11 :copyright: (C) 2010-2012 Marcin Kuzminski <marcin@python-works.com>
12 :license: GPLv3, see COPYING for more details.
12 :license: GPLv3, see COPYING for more details.
13 """
13 """
14 # This program is free software: you can redistribute it and/or modify
14 # This program is free software: you can redistribute it and/or modify
15 # it under the terms of the GNU General Public License as published by
15 # it under the terms of the GNU General Public License as published by
16 # the Free Software Foundation, either version 3 of the License, or
16 # the Free Software Foundation, either version 3 of the License, or
17 # (at your option) any later version.
17 # (at your option) any later version.
18 #
18 #
19 # This program is distributed in the hope that it will be useful,
19 # This program is distributed in the hope that it will be useful,
20 # but WITHOUT ANY WARRANTY; without even the implied warranty of
20 # but WITHOUT ANY WARRANTY; without even the implied warranty of
21 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
21 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
22 # GNU General Public License for more details.
22 # GNU General Public License for more details.
23 #
23 #
24 # You should have received a copy of the GNU General Public License
24 # You should have received a copy of the GNU General Public License
25 # along with this program. If not, see <http://www.gnu.org/licenses/>.
25 # along with this program. If not, see <http://www.gnu.org/licenses/>.
26
26
27 import os
27 import os
28 import logging
28 import logging
29 import traceback
29 import traceback
30 import urllib
30 import urllib
31
31
32 from mercurial.error import RepoError
32 from mercurial.error import RepoError
33 from mercurial.hgweb import hgweb_mod
33 from mercurial.hgweb import hgweb_mod
34
34
35 from paste.httpheaders import REMOTE_USER, AUTH_TYPE
35 from paste.httpheaders import REMOTE_USER, AUTH_TYPE
36 from webob.exc import HTTPNotFound, HTTPForbidden, HTTPInternalServerError, \
36 from webob.exc import HTTPNotFound, HTTPForbidden, HTTPInternalServerError, \
37 HTTPBadRequest, HTTPNotAcceptable
37 HTTPBadRequest, HTTPNotAcceptable
38
38
39 from rhodecode.lib.utils2 import safe_str
39 from rhodecode.lib.utils2 import safe_str
40 from rhodecode.lib.base import BaseVCSController
40 from rhodecode.lib.base import BaseVCSController
41 from rhodecode.lib.auth import get_container_username
41 from rhodecode.lib.auth import get_container_username
42 from rhodecode.lib.utils import make_ui, is_valid_repo, ui_sections
42 from rhodecode.lib.utils import make_ui, is_valid_repo, ui_sections
43 from rhodecode.lib.compat import json
43 from rhodecode.lib.compat import json
44 from rhodecode.model.db import User
44 from rhodecode.model.db import User
45 from rhodecode.lib.exceptions import HTTPLockedRC
45 from rhodecode.lib.exceptions import HTTPLockedRC
46
46
47
47
48 log = logging.getLogger(__name__)
48 log = logging.getLogger(__name__)
49
49
50
50
51 def is_mercurial(environ):
51 def is_mercurial(environ):
52 """
52 """
53 Returns True if request's target is mercurial server - header
53 Returns True if request's target is mercurial server - header
54 ``HTTP_ACCEPT`` of such request would start with ``application/mercurial``.
54 ``HTTP_ACCEPT`` of such request would start with ``application/mercurial``.
55 """
55 """
56 http_accept = environ.get('HTTP_ACCEPT')
56 http_accept = environ.get('HTTP_ACCEPT')
57 path_info = environ['PATH_INFO']
57 path_info = environ['PATH_INFO']
58 if http_accept and http_accept.startswith('application/mercurial'):
58 if http_accept and http_accept.startswith('application/mercurial'):
59 ishg_path = True
59 ishg_path = True
60 else:
60 else:
61 ishg_path = False
61 ishg_path = False
62
62
63 log.debug('pathinfo: %s detected as HG %s' % (
63 log.debug('pathinfo: %s detected as HG %s' % (
64 path_info, ishg_path)
64 path_info, ishg_path)
65 )
65 )
66 return ishg_path
66 return ishg_path
67
67
68
68
69 class SimpleHg(BaseVCSController):
69 class SimpleHg(BaseVCSController):
70
70
71 def _handle_request(self, environ, start_response):
71 def _handle_request(self, environ, start_response):
72 if not is_mercurial(environ):
72 if not is_mercurial(environ):
73 return self.application(environ, start_response)
73 return self.application(environ, start_response)
74 if not self._check_ssl(environ, start_response):
74 if not self._check_ssl(environ, start_response):
75 return HTTPNotAcceptable('SSL REQUIRED !')(environ, start_response)
75 return HTTPNotAcceptable('SSL REQUIRED !')(environ, start_response)
76
76
77 ipaddr = self._get_ip_addr(environ)
77 ipaddr = self._get_ip_addr(environ)
78 username = None
78 username = None
79 # skip passing error to error controller
79 # skip passing error to error controller
80 environ['pylons.status_code_redirect'] = True
80 environ['pylons.status_code_redirect'] = True
81
81
82 #======================================================================
82 #======================================================================
83 # EXTRACT REPOSITORY NAME FROM ENV
83 # EXTRACT REPOSITORY NAME FROM ENV
84 #======================================================================
84 #======================================================================
85 try:
85 try:
86 repo_name = environ['REPO_NAME'] = self.__get_repository(environ)
86 repo_name = environ['REPO_NAME'] = self.__get_repository(environ)
87 log.debug('Extracted repo name is %s' % repo_name)
87 log.debug('Extracted repo name is %s' % repo_name)
88 except:
88 except:
89 return HTTPInternalServerError()(environ, start_response)
89 return HTTPInternalServerError()(environ, start_response)
90
90
91 # quick check if that dir exists...
91 # quick check if that dir exists...
92 if is_valid_repo(repo_name, self.basepath, 'hg') is False:
92 if is_valid_repo(repo_name, self.basepath, 'hg') is False:
93 return HTTPNotFound()(environ, start_response)
93 return HTTPNotFound()(environ, start_response)
94
94
95 #======================================================================
95 #======================================================================
96 # GET ACTION PULL or PUSH
96 # GET ACTION PULL or PUSH
97 #======================================================================
97 #======================================================================
98 action = self.__get_action(environ)
98 action = self.__get_action(environ)
99
99
100 #======================================================================
100 #======================================================================
101 # CHECK ANONYMOUS PERMISSION
101 # CHECK ANONYMOUS PERMISSION
102 #======================================================================
102 #======================================================================
103 if action in ['pull', 'push']:
103 if action in ['pull', 'push']:
104 anonymous_user = self.__get_user('default')
104 anonymous_user = self.__get_user('default')
105 username = anonymous_user.username
105 username = anonymous_user.username
106 anonymous_perm = self._check_permission(action, anonymous_user,
106 anonymous_perm = self._check_permission(action, anonymous_user,
107 repo_name)
107 repo_name)
108
108
109 if anonymous_perm is not True or anonymous_user.active is False:
109 if anonymous_perm is not True or anonymous_user.active is False:
110 if anonymous_perm is not True:
110 if anonymous_perm is not True:
111 log.debug('Not enough credentials to access this '
111 log.debug('Not enough credentials to access this '
112 'repository as anonymous user')
112 'repository as anonymous user')
113 if anonymous_user.active is False:
113 if anonymous_user.active is False:
114 log.debug('Anonymous access is disabled, running '
114 log.debug('Anonymous access is disabled, running '
115 'authentication')
115 'authentication')
116 #==============================================================
116 #==============================================================
117 # DEFAULT PERM FAILED OR ANONYMOUS ACCESS IS DISABLED SO WE
117 # DEFAULT PERM FAILED OR ANONYMOUS ACCESS IS DISABLED SO WE
118 # NEED TO AUTHENTICATE AND ASK FOR AUTH USER PERMISSIONS
118 # NEED TO AUTHENTICATE AND ASK FOR AUTH USER PERMISSIONS
119 #==============================================================
119 #==============================================================
120
120
121 # Attempting to retrieve username from the container
121 # Attempting to retrieve username from the container
122 username = get_container_username(environ, self.config)
122 username = get_container_username(environ, self.config)
123
123
124 # If not authenticated by the container, running basic auth
124 # If not authenticated by the container, running basic auth
125 if not username:
125 if not username:
126 self.authenticate.realm = \
126 self.authenticate.realm = \
127 safe_str(self.config['rhodecode_realm'])
127 safe_str(self.config['rhodecode_realm'])
128 result = self.authenticate(environ)
128 result = self.authenticate(environ)
129 if isinstance(result, str):
129 if isinstance(result, str):
130 AUTH_TYPE.update(environ, 'basic')
130 AUTH_TYPE.update(environ, 'basic')
131 REMOTE_USER.update(environ, result)
131 REMOTE_USER.update(environ, result)
132 username = result
132 username = result
133 else:
133 else:
134 return result.wsgi_application(environ, start_response)
134 return result.wsgi_application(environ, start_response)
135
135
136 #==============================================================
136 #==============================================================
137 # CHECK PERMISSIONS FOR THIS REQUEST USING GIVEN USERNAME
137 # CHECK PERMISSIONS FOR THIS REQUEST USING GIVEN USERNAME
138 #==============================================================
138 #==============================================================
139 try:
139 try:
140 user = self.__get_user(username)
140 user = self.__get_user(username)
141 if user is None or not user.active:
141 if user is None or not user.active:
142 return HTTPForbidden()(environ, start_response)
142 return HTTPForbidden()(environ, start_response)
143 username = user.username
143 username = user.username
144 except:
144 except:
145 log.error(traceback.format_exc())
145 log.error(traceback.format_exc())
146 return HTTPInternalServerError()(environ, start_response)
146 return HTTPInternalServerError()(environ, start_response)
147
147
148 #check permissions for this repository
148 #check permissions for this repository
149 perm = self._check_permission(action, user, repo_name)
149 perm = self._check_permission(action, user, repo_name)
150 if perm is not True:
150 if perm is not True:
151 return HTTPForbidden()(environ, start_response)
151 return HTTPForbidden()(environ, start_response)
152
152
153 # extras are injected into mercurial UI object and later available
153 # extras are injected into mercurial UI object and later available
154 # in hg hooks executed by rhodecode
154 # in hg hooks executed by rhodecode
155 from rhodecode import CONFIG
155 extras = {
156 extras = {
156 'ip': ipaddr,
157 'ip': ipaddr,
157 'username': username,
158 'username': username,
158 'action': action,
159 'action': action,
159 'repository': repo_name,
160 'repository': repo_name,
160 'scm': 'hg',
161 'scm': 'hg',
162 'config': CONFIG['__file__'],
161 'make_lock': None,
163 'make_lock': None,
162 'locked_by': [None, None]
164 'locked_by': [None, None]
163 }
165 }
164 #======================================================================
166 #======================================================================
165 # MERCURIAL REQUEST HANDLING
167 # MERCURIAL REQUEST HANDLING
166 #======================================================================
168 #======================================================================
167 repo_path = os.path.join(safe_str(self.basepath), safe_str(repo_name))
169 repo_path = os.path.join(safe_str(self.basepath), safe_str(repo_name))
168 log.debug('Repository path is %s' % repo_path)
170 log.debug('Repository path is %s' % repo_path)
169
171
170 # CHECK LOCKING only if it's not ANONYMOUS USER
172 # CHECK LOCKING only if it's not ANONYMOUS USER
171 if username != User.DEFAULT_USER:
173 if username != User.DEFAULT_USER:
172 log.debug('Checking locking on repository')
174 log.debug('Checking locking on repository')
173 (make_lock,
175 (make_lock,
174 locked,
176 locked,
175 locked_by) = self._check_locking_state(
177 locked_by) = self._check_locking_state(
176 environ=environ, action=action,
178 environ=environ, action=action,
177 repo=repo_name, user_id=user.user_id
179 repo=repo_name, user_id=user.user_id
178 )
180 )
179 # store the make_lock for later evaluation in hooks
181 # store the make_lock for later evaluation in hooks
180 extras.update({'make_lock': make_lock,
182 extras.update({'make_lock': make_lock,
181 'locked_by': locked_by})
183 'locked_by': locked_by})
182
184
183 # set the environ variables for this request
185 # set the environ variables for this request
184 os.environ['RC_SCM_DATA'] = json.dumps(extras)
186 os.environ['RC_SCM_DATA'] = json.dumps(extras)
185 log.debug('HOOKS extras is %s' % extras)
187 log.debug('HOOKS extras is %s' % extras)
186 baseui = make_ui('db')
188 baseui = make_ui('db')
187 self.__inject_extras(repo_path, baseui, extras)
189 self.__inject_extras(repo_path, baseui, extras)
188
190
189 try:
191 try:
190 # invalidate cache on push
192 # invalidate cache on push
191 if action == 'push':
193 if action == 'push':
192 self._invalidate_cache(repo_name)
194 self._invalidate_cache(repo_name)
193 log.info('%s action on HG repo "%s"' % (action, repo_name))
195 log.info('%s action on HG repo "%s"' % (action, repo_name))
194 app = self.__make_app(repo_path, baseui, extras)
196 app = self.__make_app(repo_path, baseui, extras)
195 return app(environ, start_response)
197 return app(environ, start_response)
196 except RepoError, e:
198 except RepoError, e:
197 if str(e).find('not found') != -1:
199 if str(e).find('not found') != -1:
198 return HTTPNotFound()(environ, start_response)
200 return HTTPNotFound()(environ, start_response)
199 except HTTPLockedRC, e:
201 except HTTPLockedRC, e:
200 log.debug('Repositry LOCKED ret code 423!')
202 log.debug('Repositry LOCKED ret code 423!')
201 return e(environ, start_response)
203 return e(environ, start_response)
202 except Exception:
204 except Exception:
203 log.error(traceback.format_exc())
205 log.error(traceback.format_exc())
204 return HTTPInternalServerError()(environ, start_response)
206 return HTTPInternalServerError()(environ, start_response)
205
207
206 def __make_app(self, repo_name, baseui, extras):
208 def __make_app(self, repo_name, baseui, extras):
207 """
209 """
208 Make an wsgi application using hgweb, and inject generated baseui
210 Make an wsgi application using hgweb, and inject generated baseui
209 instance, additionally inject some extras into ui object
211 instance, additionally inject some extras into ui object
210 """
212 """
211 return hgweb_mod.hgweb(repo_name, name=repo_name, baseui=baseui)
213 return hgweb_mod.hgweb(repo_name, name=repo_name, baseui=baseui)
212
214
213 def __get_repository(self, environ):
215 def __get_repository(self, environ):
214 """
216 """
215 Get's repository name out of PATH_INFO header
217 Get's repository name out of PATH_INFO header
216
218
217 :param environ: environ where PATH_INFO is stored
219 :param environ: environ where PATH_INFO is stored
218 """
220 """
219 try:
221 try:
220 environ['PATH_INFO'] = self._get_by_id(environ['PATH_INFO'])
222 environ['PATH_INFO'] = self._get_by_id(environ['PATH_INFO'])
221 repo_name = '/'.join(environ['PATH_INFO'].split('/')[1:])
223 repo_name = '/'.join(environ['PATH_INFO'].split('/')[1:])
222 if repo_name.endswith('/'):
224 if repo_name.endswith('/'):
223 repo_name = repo_name.rstrip('/')
225 repo_name = repo_name.rstrip('/')
224 except:
226 except:
225 log.error(traceback.format_exc())
227 log.error(traceback.format_exc())
226 raise
228 raise
227
229
228 return repo_name
230 return repo_name
229
231
230 def __get_user(self, username):
232 def __get_user(self, username):
231 return User.get_by_username(username)
233 return User.get_by_username(username)
232
234
233 def __get_action(self, environ):
235 def __get_action(self, environ):
234 """
236 """
235 Maps mercurial request commands into a clone,pull or push command.
237 Maps mercurial request commands into a clone,pull or push command.
236 This should always return a valid command string
238 This should always return a valid command string
237
239
238 :param environ:
240 :param environ:
239 """
241 """
240 mapping = {'changegroup': 'pull',
242 mapping = {'changegroup': 'pull',
241 'changegroupsubset': 'pull',
243 'changegroupsubset': 'pull',
242 'stream_out': 'pull',
244 'stream_out': 'pull',
243 'listkeys': 'pull',
245 'listkeys': 'pull',
244 'unbundle': 'push',
246 'unbundle': 'push',
245 'pushkey': 'push', }
247 'pushkey': 'push', }
246 for qry in environ['QUERY_STRING'].split('&'):
248 for qry in environ['QUERY_STRING'].split('&'):
247 if qry.startswith('cmd'):
249 if qry.startswith('cmd'):
248 cmd = qry.split('=')[-1]
250 cmd = qry.split('=')[-1]
249 if cmd in mapping:
251 if cmd in mapping:
250 return mapping[cmd]
252 return mapping[cmd]
251
253
252 return 'pull'
254 return 'pull'
253
255
254 raise Exception('Unable to detect pull/push action !!'
256 raise Exception('Unable to detect pull/push action !!'
255 'Are you using non standard command or client ?')
257 'Are you using non standard command or client ?')
256
258
257 def __inject_extras(self, repo_path, baseui, extras={}):
259 def __inject_extras(self, repo_path, baseui, extras={}):
258 """
260 """
259 Injects some extra params into baseui instance
261 Injects some extra params into baseui instance
260
262
261 also overwrites global settings with those takes from local hgrc file
263 also overwrites global settings with those takes from local hgrc file
262
264
263 :param baseui: baseui instance
265 :param baseui: baseui instance
264 :param extras: dict with extra params to put into baseui
266 :param extras: dict with extra params to put into baseui
265 """
267 """
266
268
267 hgrc = os.path.join(repo_path, '.hg', 'hgrc')
269 hgrc = os.path.join(repo_path, '.hg', 'hgrc')
268
270
269 # make our hgweb quiet so it doesn't print output
271 # make our hgweb quiet so it doesn't print output
270 baseui.setconfig('ui', 'quiet', 'true')
272 baseui.setconfig('ui', 'quiet', 'true')
271
273
272 #inject some additional parameters that will be available in ui
274 #inject some additional parameters that will be available in ui
273 #for hooks
275 #for hooks
274 for k, v in extras.items():
276 for k, v in extras.items():
275 baseui.setconfig('rhodecode_extras', k, v)
277 baseui.setconfig('rhodecode_extras', k, v)
276
278
277 repoui = make_ui('file', hgrc, False)
279 repoui = make_ui('file', hgrc, False)
278
280
279 if repoui:
281 if repoui:
280 #overwrite our ui instance with the section from hgrc file
282 #overwrite our ui instance with the section from hgrc file
281 for section in ui_sections:
283 for section in ui_sections:
282 for k, v in repoui.configitems(section):
284 for k, v in repoui.configitems(section):
283 baseui.setconfig(section, k, v)
285 baseui.setconfig(section, k, v)
General Comments 0
You need to be logged in to leave comments. Login now