##// END OF EJS Templates
Fixed issue with gzipped streams for large git pushes.
marcink -
r2578:d24c70ec beta
parent child Browse files
Show More
@@ -1,303 +1,305 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2 """
2 """
3 rhodecode.lib.middleware.simplegit
3 rhodecode.lib.middleware.simplegit
4 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
4 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
5
5
6 SimpleGit middleware for handling git protocol request (push/clone etc.)
6 SimpleGit middleware for handling git protocol request (push/clone etc.)
7 It's implemented with basic auth function
7 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 re
28 import re
29 import logging
29 import logging
30 import traceback
30 import traceback
31
31
32 from dulwich import server as dulserver
32 from dulwich import server as dulserver
33 from dulwich.web import LimitedInputFilter, GunzipFilter
33
34
34
35
35 class SimpleGitUploadPackHandler(dulserver.UploadPackHandler):
36 class SimpleGitUploadPackHandler(dulserver.UploadPackHandler):
36
37
37 def handle(self):
38 def handle(self):
38 write = lambda x: self.proto.write_sideband(1, x)
39 write = lambda x: self.proto.write_sideband(1, x)
39
40
40 graph_walker = dulserver.ProtocolGraphWalker(self,
41 graph_walker = dulserver.ProtocolGraphWalker(self,
41 self.repo.object_store,
42 self.repo.object_store,
42 self.repo.get_peeled)
43 self.repo.get_peeled)
43 objects_iter = self.repo.fetch_objects(
44 objects_iter = self.repo.fetch_objects(
44 graph_walker.determine_wants, graph_walker, self.progress,
45 graph_walker.determine_wants, graph_walker, self.progress,
45 get_tagged=self.get_tagged)
46 get_tagged=self.get_tagged)
46
47
47 # Did the process short-circuit (e.g. in a stateless RPC call)? Note
48 # Did the process short-circuit (e.g. in a stateless RPC call)? Note
48 # that the client still expects a 0-object pack in most cases.
49 # that the client still expects a 0-object pack in most cases.
49 if objects_iter is None:
50 if objects_iter is None:
50 return
51 return
51
52
52 self.progress("counting objects: %d, done.\n" % len(objects_iter))
53 self.progress("counting objects: %d, done.\n" % len(objects_iter))
53 dulserver.write_pack_objects(dulserver.ProtocolFile(None, write),
54 dulserver.write_pack_objects(dulserver.ProtocolFile(None, write),
54 objects_iter)
55 objects_iter)
55 messages = []
56 messages = []
56 messages.append('thank you for using rhodecode')
57 messages.append('thank you for using rhodecode')
57
58
58 for msg in messages:
59 for msg in messages:
59 self.progress(msg + "\n")
60 self.progress(msg + "\n")
60 # we are done
61 # we are done
61 self.proto.write("0000")
62 self.proto.write("0000")
62
63
63
64
64 dulserver.DEFAULT_HANDLERS = {
65 dulserver.DEFAULT_HANDLERS = {
65 #git-ls-remote, git-clone, git-fetch and git-pull
66 #git-ls-remote, git-clone, git-fetch and git-pull
66 'git-upload-pack': SimpleGitUploadPackHandler,
67 'git-upload-pack': SimpleGitUploadPackHandler,
67 #git-push
68 #git-push
68 'git-receive-pack': dulserver.ReceivePackHandler,
69 'git-receive-pack': dulserver.ReceivePackHandler,
69 }
70 }
70
71
71 # not used for now until dulwich get's fixed
72 # not used for now until dulwich get's fixed
72 #from dulwich.repo import Repo
73 #from dulwich.repo import Repo
73 #from dulwich.web import make_wsgi_chain
74 #from dulwich.web import make_wsgi_chain
74
75
75 from paste.httpheaders import REMOTE_USER, AUTH_TYPE
76 from paste.httpheaders import REMOTE_USER, AUTH_TYPE
76
77
77 from rhodecode.lib.utils2 import safe_str
78 from rhodecode.lib.utils2 import safe_str
78 from rhodecode.lib.base import BaseVCSController
79 from rhodecode.lib.base import BaseVCSController
79 from rhodecode.lib.auth import get_container_username
80 from rhodecode.lib.auth import get_container_username
80 from rhodecode.lib.utils import is_valid_repo, make_ui
81 from rhodecode.lib.utils import is_valid_repo, make_ui
81 from rhodecode.model.db import User, RhodeCodeUi
82 from rhodecode.model.db import User, RhodeCodeUi
82
83
83 from webob.exc import HTTPNotFound, HTTPForbidden, HTTPInternalServerError
84 from webob.exc import HTTPNotFound, HTTPForbidden, HTTPInternalServerError
84
85
85 log = logging.getLogger(__name__)
86 log = logging.getLogger(__name__)
86
87
87
88
88 GIT_PROTO_PAT = re.compile(r'^/(.+)/(info/refs|git-upload-pack|git-receive-pack)')
89 GIT_PROTO_PAT = re.compile(r'^/(.+)/(info/refs|git-upload-pack|git-receive-pack)')
89
90
90
91
91 def is_git(environ):
92 def is_git(environ):
92 path_info = environ['PATH_INFO']
93 path_info = environ['PATH_INFO']
93 isgit_path = GIT_PROTO_PAT.match(path_info)
94 isgit_path = GIT_PROTO_PAT.match(path_info)
94 log.debug('pathinfo: %s detected as GIT %s' % (
95 log.debug('pathinfo: %s detected as GIT %s' % (
95 path_info, isgit_path != None)
96 path_info, isgit_path != None)
96 )
97 )
97 return isgit_path
98 return isgit_path
98
99
99
100
100 class SimpleGit(BaseVCSController):
101 class SimpleGit(BaseVCSController):
101
102
102 def _handle_request(self, environ, start_response):
103 def _handle_request(self, environ, start_response):
103
104
104 if not is_git(environ):
105 if not is_git(environ):
105 return self.application(environ, start_response)
106 return self.application(environ, start_response)
106
107
107 ipaddr = self._get_ip_addr(environ)
108 ipaddr = self._get_ip_addr(environ)
108 username = None
109 username = None
109 self._git_first_op = False
110 self._git_first_op = False
110 # skip passing error to error controller
111 # skip passing error to error controller
111 environ['pylons.status_code_redirect'] = True
112 environ['pylons.status_code_redirect'] = True
112
113
113 #======================================================================
114 #======================================================================
114 # EXTRACT REPOSITORY NAME FROM ENV
115 # EXTRACT REPOSITORY NAME FROM ENV
115 #======================================================================
116 #======================================================================
116 try:
117 try:
117 repo_name = self.__get_repository(environ)
118 repo_name = self.__get_repository(environ)
118 log.debug('Extracted repo name is %s' % repo_name)
119 log.debug('Extracted repo name is %s' % repo_name)
119 except:
120 except:
120 return HTTPInternalServerError()(environ, start_response)
121 return HTTPInternalServerError()(environ, start_response)
121
122
122 # quick check if that dir exists...
123 # quick check if that dir exists...
123 if is_valid_repo(repo_name, self.basepath) is False:
124 if is_valid_repo(repo_name, self.basepath) is False:
124 return HTTPNotFound()(environ, start_response)
125 return HTTPNotFound()(environ, start_response)
125
126
126 #======================================================================
127 #======================================================================
127 # GET ACTION PULL or PUSH
128 # GET ACTION PULL or PUSH
128 #======================================================================
129 #======================================================================
129 action = self.__get_action(environ)
130 action = self.__get_action(environ)
130
131
131 #======================================================================
132 #======================================================================
132 # CHECK ANONYMOUS PERMISSION
133 # CHECK ANONYMOUS PERMISSION
133 #======================================================================
134 #======================================================================
134 if action in ['pull', 'push']:
135 if action in ['pull', 'push']:
135 anonymous_user = self.__get_user('default')
136 anonymous_user = self.__get_user('default')
136 username = anonymous_user.username
137 username = anonymous_user.username
137 anonymous_perm = self._check_permission(action, anonymous_user,
138 anonymous_perm = self._check_permission(action, anonymous_user,
138 repo_name)
139 repo_name)
139
140
140 if anonymous_perm is not True or anonymous_user.active is False:
141 if anonymous_perm is not True or anonymous_user.active is False:
141 if anonymous_perm is not True:
142 if anonymous_perm is not True:
142 log.debug('Not enough credentials to access this '
143 log.debug('Not enough credentials to access this '
143 'repository as anonymous user')
144 'repository as anonymous user')
144 if anonymous_user.active is False:
145 if anonymous_user.active is False:
145 log.debug('Anonymous access is disabled, running '
146 log.debug('Anonymous access is disabled, running '
146 'authentication')
147 'authentication')
147 #==============================================================
148 #==============================================================
148 # DEFAULT PERM FAILED OR ANONYMOUS ACCESS IS DISABLED SO WE
149 # DEFAULT PERM FAILED OR ANONYMOUS ACCESS IS DISABLED SO WE
149 # NEED TO AUTHENTICATE AND ASK FOR AUTH USER PERMISSIONS
150 # NEED TO AUTHENTICATE AND ASK FOR AUTH USER PERMISSIONS
150 #==============================================================
151 #==============================================================
151
152
152 # Attempting to retrieve username from the container
153 # Attempting to retrieve username from the container
153 username = get_container_username(environ, self.config)
154 username = get_container_username(environ, self.config)
154
155
155 # If not authenticated by the container, running basic auth
156 # If not authenticated by the container, running basic auth
156 if not username:
157 if not username:
157 self.authenticate.realm = \
158 self.authenticate.realm = \
158 safe_str(self.config['rhodecode_realm'])
159 safe_str(self.config['rhodecode_realm'])
159 result = self.authenticate(environ)
160 result = self.authenticate(environ)
160 if isinstance(result, str):
161 if isinstance(result, str):
161 AUTH_TYPE.update(environ, 'basic')
162 AUTH_TYPE.update(environ, 'basic')
162 REMOTE_USER.update(environ, result)
163 REMOTE_USER.update(environ, result)
163 username = result
164 username = result
164 else:
165 else:
165 return result.wsgi_application(environ, start_response)
166 return result.wsgi_application(environ, start_response)
166
167
167 #==============================================================
168 #==============================================================
168 # CHECK PERMISSIONS FOR THIS REQUEST USING GIVEN USERNAME
169 # CHECK PERMISSIONS FOR THIS REQUEST USING GIVEN USERNAME
169 #==============================================================
170 #==============================================================
170 try:
171 try:
171 user = self.__get_user(username)
172 user = self.__get_user(username)
172 if user is None or not user.active:
173 if user is None or not user.active:
173 return HTTPForbidden()(environ, start_response)
174 return HTTPForbidden()(environ, start_response)
174 username = user.username
175 username = user.username
175 except:
176 except:
176 log.error(traceback.format_exc())
177 log.error(traceback.format_exc())
177 return HTTPInternalServerError()(environ, start_response)
178 return HTTPInternalServerError()(environ, start_response)
178
179
179 #check permissions for this repository
180 #check permissions for this repository
180 perm = self._check_permission(action, user, repo_name)
181 perm = self._check_permission(action, user, repo_name)
181 if perm is not True:
182 if perm is not True:
182 return HTTPForbidden()(environ, start_response)
183 return HTTPForbidden()(environ, start_response)
183
184
184 extras = {
185 extras = {
185 'ip': ipaddr,
186 'ip': ipaddr,
186 'username': username,
187 'username': username,
187 'action': action,
188 'action': action,
188 'repository': repo_name,
189 'repository': repo_name,
189 'scm': 'git',
190 'scm': 'git',
190 }
191 }
191
192
192 #===================================================================
193 #===================================================================
193 # GIT REQUEST HANDLING
194 # GIT REQUEST HANDLING
194 #===================================================================
195 #===================================================================
195 repo_path = os.path.join(safe_str(self.basepath), safe_str(repo_name))
196 repo_path = os.path.join(safe_str(self.basepath), safe_str(repo_name))
196 log.debug('Repository path is %s' % repo_path)
197 log.debug('Repository path is %s' % repo_path)
197
198
198 baseui = make_ui('db')
199 baseui = make_ui('db')
199 self.__inject_extras(repo_path, baseui, extras)
200 self.__inject_extras(repo_path, baseui, extras)
200
201
201 try:
202 try:
202 # invalidate cache on push
203 # invalidate cache on push
203 if action == 'push':
204 if action == 'push':
204 self._invalidate_cache(repo_name)
205 self._invalidate_cache(repo_name)
205 self._handle_githooks(repo_name, action, baseui, environ)
206 self._handle_githooks(repo_name, action, baseui, environ)
206
207
207 log.info('%s action on GIT repo "%s"' % (action, repo_name))
208 log.info('%s action on GIT repo "%s"' % (action, repo_name))
208 app = self.__make_app(repo_name, repo_path, username)
209 app = self.__make_app(repo_name, repo_path, username)
209 return app(environ, start_response)
210 return app(environ, start_response)
210 except Exception:
211 except Exception:
211 log.error(traceback.format_exc())
212 log.error(traceback.format_exc())
212 return HTTPInternalServerError()(environ, start_response)
213 return HTTPInternalServerError()(environ, start_response)
213
214
214 def __make_app(self, repo_name, repo_path, username):
215 def __make_app(self, repo_name, repo_path, username):
215 """
216 """
216 Make an wsgi application using dulserver
217 Make an wsgi application using dulserver
217
218
218 :param repo_name: name of the repository
219 :param repo_name: name of the repository
219 :param repo_path: full path to the repository
220 :param repo_path: full path to the repository
220 """
221 """
221
222
222 from rhodecode.lib.middleware.pygrack import make_wsgi_app
223 from rhodecode.lib.middleware.pygrack import make_wsgi_app
223 app = make_wsgi_app(
224 app = make_wsgi_app(
224 repo_root=safe_str(self.basepath),
225 repo_root=safe_str(self.basepath),
225 repo_name=repo_name,
226 repo_name=repo_name,
226 username=username,
227 username=username,
227 )
228 )
229 app = GunzipFilter(LimitedInputFilter(app))
228 return app
230 return app
229
231
230 def __get_repository(self, environ):
232 def __get_repository(self, environ):
231 """
233 """
232 Get's repository name out of PATH_INFO header
234 Get's repository name out of PATH_INFO header
233
235
234 :param environ: environ where PATH_INFO is stored
236 :param environ: environ where PATH_INFO is stored
235 """
237 """
236 try:
238 try:
237 environ['PATH_INFO'] = self._get_by_id(environ['PATH_INFO'])
239 environ['PATH_INFO'] = self._get_by_id(environ['PATH_INFO'])
238 repo_name = GIT_PROTO_PAT.match(environ['PATH_INFO']).group(1)
240 repo_name = GIT_PROTO_PAT.match(environ['PATH_INFO']).group(1)
239 except:
241 except:
240 log.error(traceback.format_exc())
242 log.error(traceback.format_exc())
241 raise
243 raise
242
244
243 return repo_name
245 return repo_name
244
246
245 def __get_user(self, username):
247 def __get_user(self, username):
246 return User.get_by_username(username)
248 return User.get_by_username(username)
247
249
248 def __get_action(self, environ):
250 def __get_action(self, environ):
249 """
251 """
250 Maps git request commands into a pull or push command.
252 Maps git request commands into a pull or push command.
251
253
252 :param environ:
254 :param environ:
253 """
255 """
254 service = environ['QUERY_STRING'].split('=')
256 service = environ['QUERY_STRING'].split('=')
255
257
256 if len(service) > 1:
258 if len(service) > 1:
257 service_cmd = service[1]
259 service_cmd = service[1]
258 mapping = {
260 mapping = {
259 'git-receive-pack': 'push',
261 'git-receive-pack': 'push',
260 'git-upload-pack': 'pull',
262 'git-upload-pack': 'pull',
261 }
263 }
262 op = mapping[service_cmd]
264 op = mapping[service_cmd]
263 self._git_stored_op = op
265 self._git_stored_op = op
264 return op
266 return op
265 else:
267 else:
266 # try to fallback to stored variable as we don't know if the last
268 # try to fallback to stored variable as we don't know if the last
267 # operation is pull/push
269 # operation is pull/push
268 op = getattr(self, '_git_stored_op', 'pull')
270 op = getattr(self, '_git_stored_op', 'pull')
269 return op
271 return op
270
272
271 def _handle_githooks(self, repo_name, action, baseui, environ):
273 def _handle_githooks(self, repo_name, action, baseui, environ):
272 """
274 """
273 Handles pull action, push is handled by post-receive hook
275 Handles pull action, push is handled by post-receive hook
274 """
276 """
275 from rhodecode.lib.hooks import log_pull_action
277 from rhodecode.lib.hooks import log_pull_action
276 service = environ['QUERY_STRING'].split('=')
278 service = environ['QUERY_STRING'].split('=')
277 if len(service) < 2:
279 if len(service) < 2:
278 return
280 return
279
281
280 from rhodecode.model.db import Repository
282 from rhodecode.model.db import Repository
281 _repo = Repository.get_by_repo_name(repo_name)
283 _repo = Repository.get_by_repo_name(repo_name)
282 _repo = _repo.scm_instance
284 _repo = _repo.scm_instance
283 _repo._repo.ui = baseui
285 _repo._repo.ui = baseui
284
286
285 _hooks = dict(baseui.configitems('hooks')) or {}
287 _hooks = dict(baseui.configitems('hooks')) or {}
286 if action == 'pull' and _hooks.get(RhodeCodeUi.HOOK_PULL):
288 if action == 'pull' and _hooks.get(RhodeCodeUi.HOOK_PULL):
287 log_pull_action(ui=baseui, repo=_repo._repo)
289 log_pull_action(ui=baseui, repo=_repo._repo)
288
290
289 def __inject_extras(self, repo_path, baseui, extras={}):
291 def __inject_extras(self, repo_path, baseui, extras={}):
290 """
292 """
291 Injects some extra params into baseui instance
293 Injects some extra params into baseui instance
292
294
293 :param baseui: baseui instance
295 :param baseui: baseui instance
294 :param extras: dict with extra params to put into baseui
296 :param extras: dict with extra params to put into baseui
295 """
297 """
296
298
297 # make our hgweb quiet so it doesn't print output
299 # make our hgweb quiet so it doesn't print output
298 baseui.setconfig('ui', 'quiet', 'true')
300 baseui.setconfig('ui', 'quiet', 'true')
299
301
300 #inject some additional parameters that will be available in ui
302 #inject some additional parameters that will be available in ui
301 #for hooks
303 #for hooks
302 for k, v in extras.items():
304 for k, v in extras.items():
303 baseui.setconfig('rhodecode_extras', k, v)
305 baseui.setconfig('rhodecode_extras', k, v)
General Comments 0
You need to be logged in to leave comments. Login now