##// END OF EJS Templates
backported fixes for issue #318...
marcink -
r1834:d0e477b5 default
parent child Browse files
Show More
@@ -1,291 +1,302
1 1 # -*- coding: utf-8 -*-
2 2 """
3 3 rhodecode.lib.middleware.simplegit
4 4 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
5 5
6 6 SimpleGit middleware for handling git protocol request (push/clone etc.)
7 7 It's implemented with basic auth function
8 8
9 9 :created_on: Apr 28, 2010
10 10 :author: marcink
11 11 :copyright: (C) 2009-2010 Marcin Kuzminski <marcin@python-works.com>
12 12 :license: GPLv3, see COPYING for more details.
13 13 """
14 14 # This program is free software: you can redistribute it and/or modify
15 15 # it under the terms of the GNU General Public License as published by
16 16 # the Free Software Foundation, either version 3 of the License, or
17 17 # (at your option) any later version.
18 18 #
19 19 # This program is distributed in the hope that it will be useful,
20 20 # but WITHOUT ANY WARRANTY; without even the implied warranty of
21 21 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
22 22 # GNU General Public License for more details.
23 23 #
24 24 # You should have received a copy of the GNU General Public License
25 25 # along with this program. If not, see <http://www.gnu.org/licenses/>.
26 26
27 27 import os
28 28 import logging
29 29 import traceback
30 import time
30 31
31 32 from dulwich import server as dulserver
32 33
33 34
34 35 class SimpleGitUploadPackHandler(dulserver.UploadPackHandler):
35 36
36 37 def handle(self):
37 38 write = lambda x: self.proto.write_sideband(1, x)
38 39
39 40 graph_walker = dulserver.ProtocolGraphWalker(self,
40 41 self.repo.object_store,
41 42 self.repo.get_peeled)
42 43 objects_iter = self.repo.fetch_objects(
43 44 graph_walker.determine_wants, graph_walker, self.progress,
44 45 get_tagged=self.get_tagged)
45 46
46 47 # Do they want any objects?
47 48 if objects_iter is None or len(objects_iter) == 0:
48 49 return
49 50
50 51 self.progress("counting objects: %d, done.\n" % len(objects_iter))
51 52 dulserver.write_pack_objects(dulserver.ProtocolFile(None, write),
52 53 objects_iter, len(objects_iter))
53 54 messages = []
54 55 messages.append('thank you for using rhodecode')
55 56
56 57 for msg in messages:
57 58 self.progress(msg + "\n")
58 59 # we are done
59 60 self.proto.write("0000")
60 61
61 62 dulserver.DEFAULT_HANDLERS = {
62 63 'git-upload-pack': SimpleGitUploadPackHandler,
63 64 'git-receive-pack': dulserver.ReceivePackHandler,
64 65 }
65 66
66 67 from dulwich.repo import Repo
67 68 from dulwich.web import HTTPGitApplication
68 69
69 70 from paste.auth.basic import AuthBasicAuthenticator
70 71 from paste.httpheaders import REMOTE_USER, AUTH_TYPE
71 72
72 73 from rhodecode.lib import safe_str
73 74 from rhodecode.lib.auth import authfunc, HasPermissionAnyMiddleware
74 75 from rhodecode.lib.utils import invalidate_cache, is_valid_repo
76 from rhodecode.model import meta
75 77 from rhodecode.model.db import User
76 78
77 79 from webob.exc import HTTPNotFound, HTTPForbidden, HTTPInternalServerError
78 80
79 81 log = logging.getLogger(__name__)
80 82
81 83
82 84 def is_git(environ):
83 85 """Returns True if request's target is git server.
84 86 ``HTTP_USER_AGENT`` would then have git client version given.
85 87
86 88 :param environ:
87 89 """
88 90 http_user_agent = environ.get('HTTP_USER_AGENT')
89 91 if http_user_agent and http_user_agent.startswith('git'):
90 92 return True
91 93 return False
92 94
93 95
94 96 class SimpleGit(object):
95 97
96 98 def __init__(self, application, config):
97 99 self.application = application
98 100 self.config = config
99 101 # base path of repo locations
100 102 self.basepath = self.config['base_path']
101 103 #authenticate this mercurial request using authfunc
102 104 self.authenticate = AuthBasicAuthenticator('', authfunc)
103 105
104 106 def __call__(self, environ, start_response):
107 start = time.time()
108 try:
109 return self._handle_request(environ, start_response)
110 finally:
111 log = logging.getLogger(self.__class__.__name__)
112 log.debug('Request time: %.3fs' % (time.time() - start))
113 meta.Session.remove()
114
115 def _handle_request(self, environ, start_response):
105 116 if not is_git(environ):
106 117 return self.application(environ, start_response)
107 118
108 119 proxy_key = 'HTTP_X_REAL_IP'
109 120 def_key = 'REMOTE_ADDR'
110 121 ipaddr = environ.get(proxy_key, environ.get(def_key, '0.0.0.0'))
111 122 username = None
112 123 # skip passing error to error controller
113 124 environ['pylons.status_code_redirect'] = True
114 125
115 126 #======================================================================
116 127 # EXTRACT REPOSITORY NAME FROM ENV
117 128 #======================================================================
118 129 try:
119 130 repo_name = self.__get_repository(environ)
120 131 log.debug('Extracted repo name is %s' % repo_name)
121 132 except:
122 133 return HTTPInternalServerError()(environ, start_response)
123 134
124 135 #======================================================================
125 136 # GET ACTION PULL or PUSH
126 137 #======================================================================
127 138 action = self.__get_action(environ)
128 139
129 140 #======================================================================
130 141 # CHECK ANONYMOUS PERMISSION
131 142 #======================================================================
132 143 if action in ['pull', 'push']:
133 144 anonymous_user = self.__get_user('default')
134 145 username = anonymous_user.username
135 146 anonymous_perm = self.__check_permission(action,
136 147 anonymous_user,
137 148 repo_name)
138 149
139 150 if anonymous_perm is not True or anonymous_user.active is False:
140 151 if anonymous_perm is not True:
141 152 log.debug('Not enough credentials to access this '
142 153 'repository as anonymous user')
143 154 if anonymous_user.active is False:
144 155 log.debug('Anonymous access is disabled, running '
145 156 'authentication')
146 157 #==============================================================
147 158 # DEFAULT PERM FAILED OR ANONYMOUS ACCESS IS DISABLED SO WE
148 159 # NEED TO AUTHENTICATE AND ASK FOR AUTH USER PERMISSIONS
149 160 #==============================================================
150 161
151 162 if not REMOTE_USER(environ):
152 163 self.authenticate.realm = \
153 164 safe_str(self.config['rhodecode_realm'])
154 165 result = self.authenticate(environ)
155 166 if isinstance(result, str):
156 167 AUTH_TYPE.update(environ, 'basic')
157 168 REMOTE_USER.update(environ, result)
158 169 else:
159 170 return result.wsgi_application(environ, start_response)
160 171
161 172 #==============================================================
162 173 # CHECK PERMISSIONS FOR THIS REQUEST USING GIVEN USERNAME FROM
163 174 # BASIC AUTH
164 175 #==============================================================
165 176
166 177 if action in ['pull', 'push']:
167 178 username = REMOTE_USER(environ)
168 179 try:
169 180 user = self.__get_user(username)
170 181 if user is None:
171 182 return HTTPForbidden()(environ, start_response)
172 183 username = user.username
173 184 except:
174 185 log.error(traceback.format_exc())
175 186 return HTTPInternalServerError()(environ,
176 187 start_response)
177 188
178 189 #check permissions for this repository
179 190 perm = self.__check_permission(action, user,
180 191 repo_name)
181 192 if perm is not True:
182 193 return HTTPForbidden()(environ, start_response)
183 194
184 195 extras = {'ip': ipaddr,
185 196 'username': username,
186 197 'action': action,
187 198 'repository': repo_name}
188 199
189 200 #===================================================================
190 201 # GIT REQUEST HANDLING
191 202 #===================================================================
192 203
193 204 repo_path = safe_str(os.path.join(self.basepath, repo_name))
194 205 log.debug('Repository path is %s' % repo_path)
195 206
196 207 # quick check if that dir exists...
197 208 if is_valid_repo(repo_name, self.basepath) is False:
198 209 return HTTPNotFound()(environ, start_response)
199 210
200 211 try:
201 212 #invalidate cache on push
202 213 if action == 'push':
203 214 self.__invalidate_cache(repo_name)
204 215
205 216 app = self.__make_app(repo_name, repo_path)
206 217 return app(environ, start_response)
207 218 except Exception:
208 219 log.error(traceback.format_exc())
209 220 return HTTPInternalServerError()(environ, start_response)
210 221
211 222 def __make_app(self, repo_name, repo_path):
212 223 """
213 224 Make an wsgi application using dulserver
214 225
215 226 :param repo_name: name of the repository
216 227 :param repo_path: full path to the repository
217 228 """
218 229
219 230 _d = {'/' + repo_name: Repo(repo_path)}
220 231 backend = dulserver.DictBackend(_d)
221 232 gitserve = HTTPGitApplication(backend)
222 233
223 234 return gitserve
224 235
225 236 def __check_permission(self, action, user, repo_name):
226 237 """
227 238 Checks permissions using action (push/pull) user and repository
228 239 name
229 240
230 241 :param action: push or pull action
231 242 :param user: user instance
232 243 :param repo_name: repository name
233 244 """
234 245 if action == 'push':
235 246 if not HasPermissionAnyMiddleware('repository.write',
236 247 'repository.admin')(user,
237 248 repo_name):
238 249 return False
239 250
240 251 else:
241 252 #any other action need at least read permission
242 253 if not HasPermissionAnyMiddleware('repository.read',
243 254 'repository.write',
244 255 'repository.admin')(user,
245 256 repo_name):
246 257 return False
247 258
248 259 return True
249 260
250 261 def __get_repository(self, environ):
251 262 """
252 263 Get's repository name out of PATH_INFO header
253 264
254 265 :param environ: environ where PATH_INFO is stored
255 266 """
256 267 try:
257 268 repo_name = '/'.join(environ['PATH_INFO'].split('/')[1:])
258 269 if repo_name.endswith('/'):
259 270 repo_name = repo_name.rstrip('/')
260 271 except:
261 272 log.error(traceback.format_exc())
262 273 raise
263 274 repo_name = repo_name.split('/')[0]
264 275 return repo_name
265 276
266 277 def __get_user(self, username):
267 278 return User.get_by_username(username)
268 279
269 280 def __get_action(self, environ):
270 281 """Maps git request commands into a pull or push command.
271 282
272 283 :param environ:
273 284 """
274 285 service = environ['QUERY_STRING'].split('=')
275 286 if len(service) > 1:
276 287 service_cmd = service[1]
277 288 mapping = {'git-receive-pack': 'push',
278 289 'git-upload-pack': 'pull',
279 290 }
280 291
281 292 return mapping.get(service_cmd,
282 293 service_cmd if service_cmd else 'other')
283 294 else:
284 295 return 'other'
285 296
286 297 def __invalidate_cache(self, repo_name):
287 298 """we know that some change was made to repositories and we should
288 299 invalidate the cache to see the changes right away but only for
289 300 push requests"""
290 301 invalidate_cache('get_repo_cached_%s' % repo_name)
291 302
@@ -1,290 +1,301
1 1 # -*- coding: utf-8 -*-
2 2 """
3 3 rhodecode.lib.middleware.simplehg
4 4 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
5 5
6 6 SimpleHG middleware for handling mercurial protocol request
7 7 (push/clone etc.). It's implemented with basic auth function
8 8
9 9 :created_on: Apr 28, 2010
10 10 :author: marcink
11 11 :copyright: (C) 2009-2010 Marcin Kuzminski <marcin@python-works.com>
12 12 :license: GPLv3, see COPYING for more details.
13 13 """
14 14 # This program is free software: you can redistribute it and/or modify
15 15 # it under the terms of the GNU General Public License as published by
16 16 # the Free Software Foundation, either version 3 of the License, or
17 17 # (at your option) any later version.
18 18 #
19 19 # This program is distributed in the hope that it will be useful,
20 20 # but WITHOUT ANY WARRANTY; without even the implied warranty of
21 21 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
22 22 # GNU General Public License for more details.
23 23 #
24 24 # You should have received a copy of the GNU General Public License
25 25 # along with this program. If not, see <http://www.gnu.org/licenses/>.
26 26
27 27 import os
28 28 import logging
29 29 import traceback
30 import time
30 31
31 32 from mercurial.error import RepoError
32 33 from mercurial.hgweb import hgweb_mod
33 34
34 35 from paste.auth.basic import AuthBasicAuthenticator
35 36 from paste.httpheaders import REMOTE_USER, AUTH_TYPE
36 37
37 38 from rhodecode.lib import safe_str
38 39 from rhodecode.lib.auth import authfunc, HasPermissionAnyMiddleware
39 40 from rhodecode.lib.utils import make_ui, invalidate_cache, \
40 41 is_valid_repo, ui_sections
42 from rhodecode.model import meta
41 43 from rhodecode.model.db import User
42 44
43 45 from webob.exc import HTTPNotFound, HTTPForbidden, HTTPInternalServerError
44 46
45 47 log = logging.getLogger(__name__)
46 48
47 49
48 50 def is_mercurial(environ):
49 51 """Returns True if request's target is mercurial server - header
50 52 ``HTTP_ACCEPT`` of such request would start with ``application/mercurial``.
51 53 """
52 54 http_accept = environ.get('HTTP_ACCEPT')
53 55 if http_accept and http_accept.startswith('application/mercurial'):
54 56 return True
55 57 return False
56 58
57 59
58 60 class SimpleHg(object):
59 61
60 62 def __init__(self, application, config):
61 63 self.application = application
62 64 self.config = config
63 65 # base path of repo locations
64 66 self.basepath = self.config['base_path']
65 67 #authenticate this mercurial request using authfunc
66 68 self.authenticate = AuthBasicAuthenticator('', authfunc)
67 69 self.ipaddr = '0.0.0.0'
68 70
69 71 def __call__(self, environ, start_response):
72 start = time.time()
73 try:
74 return self._handle_request(environ, start_response)
75 finally:
76 log = logging.getLogger(self.__class__.__name__)
77 log.debug('Request time: %.3fs' % (time.time() - start))
78 meta.Session.remove()
79
80 def _handle_request(self, environ, start_response):
70 81 if not is_mercurial(environ):
71 82 return self.application(environ, start_response)
72 83
73 84 proxy_key = 'HTTP_X_REAL_IP'
74 85 def_key = 'REMOTE_ADDR'
75 86 ipaddr = environ.get(proxy_key, environ.get(def_key, '0.0.0.0'))
76 87
77 88 # skip passing error to error controller
78 89 environ['pylons.status_code_redirect'] = True
79 90
80 91 #======================================================================
81 92 # EXTRACT REPOSITORY NAME FROM ENV
82 93 #======================================================================
83 94 try:
84 95 repo_name = environ['REPO_NAME'] = self.__get_repository(environ)
85 96 log.debug('Extracted repo name is %s' % repo_name)
86 97 except:
87 98 return HTTPInternalServerError()(environ, start_response)
88 99
89 100 #======================================================================
90 101 # GET ACTION PULL or PUSH
91 102 #======================================================================
92 103 action = self.__get_action(environ)
93 104
94 105 #======================================================================
95 106 # CHECK ANONYMOUS PERMISSION
96 107 #======================================================================
97 108 if action in ['pull', 'push']:
98 109 anonymous_user = self.__get_user('default')
99 110
100 111 username = anonymous_user.username
101 112 anonymous_perm = self.__check_permission(action,
102 113 anonymous_user,
103 114 repo_name)
104 115
105 116 if anonymous_perm is not True or anonymous_user.active is False:
106 117 if anonymous_perm is not True:
107 118 log.debug('Not enough credentials to access this '
108 119 'repository as anonymous user')
109 120 if anonymous_user.active is False:
110 121 log.debug('Anonymous access is disabled, running '
111 122 'authentication')
112 123 #==============================================================
113 124 # DEFAULT PERM FAILED OR ANONYMOUS ACCESS IS DISABLED SO WE
114 125 # NEED TO AUTHENTICATE AND ASK FOR AUTH USER PERMISSIONS
115 126 #==============================================================
116 127
117 128 if not REMOTE_USER(environ):
118 129 self.authenticate.realm = \
119 130 safe_str(self.config['rhodecode_realm'])
120 131 result = self.authenticate(environ)
121 132 if isinstance(result, str):
122 133 AUTH_TYPE.update(environ, 'basic')
123 134 REMOTE_USER.update(environ, result)
124 135 else:
125 136 return result.wsgi_application(environ, start_response)
126 137
127 138 #==============================================================
128 139 # CHECK PERMISSIONS FOR THIS REQUEST USING GIVEN USERNAME FROM
129 140 # BASIC AUTH
130 141 #==============================================================
131 142
132 143 if action in ['pull', 'push']:
133 144 username = REMOTE_USER(environ)
134 145 try:
135 146 user = self.__get_user(username)
136 147 if user is None:
137 148 return HTTPForbidden()(environ, start_response)
138 149 username = user.username
139 150 except:
140 151 log.error(traceback.format_exc())
141 152 return HTTPInternalServerError()(environ,
142 153 start_response)
143 154
144 155 #check permissions for this repository
145 156 perm = self.__check_permission(action, user,
146 157 repo_name)
147 158 if perm is not True:
148 159 return HTTPForbidden()(environ, start_response)
149 160
150 161 extras = {'ip': ipaddr,
151 162 'username': username,
152 163 'action': action,
153 164 'repository': repo_name}
154 165
155 166 #======================================================================
156 167 # MERCURIAL REQUEST HANDLING
157 168 #======================================================================
158 169
159 170 repo_path = safe_str(os.path.join(self.basepath, repo_name))
160 171 log.debug('Repository path is %s' % repo_path)
161 172
162 173 baseui = make_ui('db')
163 174 self.__inject_extras(repo_path, baseui, extras)
164 175
165 176
166 177 # quick check if that dir exists...
167 178 if is_valid_repo(repo_name, self.basepath) is False:
168 179 return HTTPNotFound()(environ, start_response)
169 180
170 181 try:
171 182 #invalidate cache on push
172 183 if action == 'push':
173 184 self.__invalidate_cache(repo_name)
174 185
175 186 app = self.__make_app(repo_path, baseui, extras)
176 187 return app(environ, start_response)
177 188 except RepoError, e:
178 189 if str(e).find('not found') != -1:
179 190 return HTTPNotFound()(environ, start_response)
180 191 except Exception:
181 192 log.error(traceback.format_exc())
182 193 return HTTPInternalServerError()(environ, start_response)
183 194
184 195 def __make_app(self, repo_name, baseui, extras):
185 196 """
186 197 Make an wsgi application using hgweb, and inject generated baseui
187 198 instance, additionally inject some extras into ui object
188 199 """
189 200 return hgweb_mod.hgweb(repo_name, name=repo_name, baseui=baseui)
190 201
191 202
192 203 def __check_permission(self, action, user, repo_name):
193 204 """
194 205 Checks permissions using action (push/pull) user and repository
195 206 name
196 207
197 208 :param action: push or pull action
198 209 :param user: user instance
199 210 :param repo_name: repository name
200 211 """
201 212 if action == 'push':
202 213 if not HasPermissionAnyMiddleware('repository.write',
203 214 'repository.admin')(user,
204 215 repo_name):
205 216 return False
206 217
207 218 else:
208 219 #any other action need at least read permission
209 220 if not HasPermissionAnyMiddleware('repository.read',
210 221 'repository.write',
211 222 'repository.admin')(user,
212 223 repo_name):
213 224 return False
214 225
215 226 return True
216 227
217 228 def __get_repository(self, environ):
218 229 """
219 230 Get's repository name out of PATH_INFO header
220 231
221 232 :param environ: environ where PATH_INFO is stored
222 233 """
223 234 try:
224 235 repo_name = '/'.join(environ['PATH_INFO'].split('/')[1:])
225 236 if repo_name.endswith('/'):
226 237 repo_name = repo_name.rstrip('/')
227 238 except:
228 239 log.error(traceback.format_exc())
229 240 raise
230 241
231 242 return repo_name
232 243
233 244 def __get_user(self, username):
234 245 return User.get_by_username(username)
235 246
236 247 def __get_action(self, environ):
237 248 """
238 249 Maps mercurial request commands into a clone,pull or push command.
239 250 This should always return a valid command string
240 251
241 252 :param environ:
242 253 """
243 254 mapping = {'changegroup': 'pull',
244 255 'changegroupsubset': 'pull',
245 256 'stream_out': 'pull',
246 257 'listkeys': 'pull',
247 258 'unbundle': 'push',
248 259 'pushkey': 'push', }
249 260 for qry in environ['QUERY_STRING'].split('&'):
250 261 if qry.startswith('cmd'):
251 262 cmd = qry.split('=')[-1]
252 263 if cmd in mapping:
253 264 return mapping[cmd]
254 265 else:
255 266 return 'pull'
256 267
257 268 def __invalidate_cache(self, repo_name):
258 269 """we know that some change was made to repositories and we should
259 270 invalidate the cache to see the changes right away but only for
260 271 push requests"""
261 272 invalidate_cache('get_repo_cached_%s' % repo_name)
262 273
263 274 def __inject_extras(self, repo_path, baseui, extras={}):
264 275 """
265 276 Injects some extra params into baseui instance
266 277
267 278 also overwrites global settings with those takes from local hgrc file
268 279
269 280 :param baseui: baseui instance
270 281 :param extras: dict with extra params to put into baseui
271 282 """
272 283
273 284 hgrc = os.path.join(repo_path, '.hg', 'hgrc')
274 285
275 286 # make our hgweb quiet so it doesn't print output
276 287 baseui.setconfig('ui', 'quiet', 'true')
277 288
278 289 #inject some additional parameters that will be available in ui
279 290 #for hooks
280 291 for k, v in extras.items():
281 292 baseui.setconfig('rhodecode_extras', k, v)
282 293
283 294 repoui = make_ui('file', hgrc, False)
284 295
285 296 if repoui:
286 297 #overwrite our ui instance with the section from hgrc file
287 298 for section in ui_sections:
288 299 for k, v in repoui.configitems(section):
289 300 baseui.setconfig(section, k, v)
290 301
General Comments 0
You need to be logged in to leave comments. Login now