##// END OF EJS Templates
Rewrote git middleware with the same pattern as recent fix for #176...
marcink -
r1496:f4fed0b3 beta
parent child Browse files
Show More
@@ -1,82 +1,84
1 """Pylons middleware initialization"""
1 """Pylons middleware initialization"""
2
2
3 from beaker.middleware import SessionMiddleware
3 from beaker.middleware import SessionMiddleware
4 from routes.middleware import RoutesMiddleware
4 from routes.middleware import RoutesMiddleware
5 from paste.cascade import Cascade
5 from paste.cascade import Cascade
6 from paste.registry import RegistryManager
6 from paste.registry import RegistryManager
7 from paste.urlparser import StaticURLParser
7 from paste.urlparser import StaticURLParser
8 from paste.deploy.converters import asbool
8 from paste.deploy.converters import asbool
9 from paste.gzipper import make_gzip_middleware
9 from paste.gzipper import make_gzip_middleware
10
10
11 from pylons.middleware import ErrorHandler, StatusCodeRedirect
11 from pylons.middleware import ErrorHandler, StatusCodeRedirect
12 from pylons.wsgiapp import PylonsApp
12 from pylons.wsgiapp import PylonsApp
13
13
14 from rhodecode.lib.middleware.simplehg import SimpleHg
14 from rhodecode.lib.middleware.simplehg import SimpleHg
15 from rhodecode.lib.middleware.simplegit import SimpleGit
15 from rhodecode.lib.middleware.simplegit import SimpleGit
16 from rhodecode.lib.middleware.https_fixup import HttpsFixup
16 from rhodecode.lib.middleware.https_fixup import HttpsFixup
17 from rhodecode.config.environment import load_environment
17 from rhodecode.config.environment import load_environment
18
18
19
19
20 def make_app(global_conf, full_stack=True, static_files=True, **app_conf):
20 def make_app(global_conf, full_stack=True, static_files=True, **app_conf):
21 """Create a Pylons WSGI application and return it
21 """Create a Pylons WSGI application and return it
22
22
23 ``global_conf``
23 ``global_conf``
24 The inherited configuration for this application. Normally from
24 The inherited configuration for this application. Normally from
25 the [DEFAULT] section of the Paste ini file.
25 the [DEFAULT] section of the Paste ini file.
26
26
27 ``full_stack``
27 ``full_stack``
28 Whether or not this application provides a full WSGI stack (by
28 Whether or not this application provides a full WSGI stack (by
29 default, meaning it handles its own exceptions and errors).
29 default, meaning it handles its own exceptions and errors).
30 Disable full_stack when this application is "managed" by
30 Disable full_stack when this application is "managed" by
31 another WSGI middleware.
31 another WSGI middleware.
32
32
33 ``app_conf``
33 ``app_conf``
34 The application's local configuration. Normally specified in
34 The application's local configuration. Normally specified in
35 the [app:<name>] section of the Paste ini file (where <name>
35 the [app:<name>] section of the Paste ini file (where <name>
36 defaults to main).
36 defaults to main).
37
37
38 """
38 """
39 # Configure the Pylons environment
39 # Configure the Pylons environment
40 config = load_environment(global_conf, app_conf)
40 config = load_environment(global_conf, app_conf)
41
41
42 # The Pylons WSGI app
42 # The Pylons WSGI app
43 app = PylonsApp(config=config)
43 app = PylonsApp(config=config)
44
44
45 # Routing/Session/Cache Middleware
45 # Routing/Session/Cache Middleware
46 app = RoutesMiddleware(app, config['routes.map'])
46 app = RoutesMiddleware(app, config['routes.map'])
47 app = SessionMiddleware(app, config)
47 app = SessionMiddleware(app, config)
48
48
49 # CUSTOM MIDDLEWARE HERE (filtered by error handling middlewares)
49 # CUSTOM MIDDLEWARE HERE (filtered by error handling middlewares)
50 if asbool(config['pdebug']):
50 if asbool(config['pdebug']):
51 from rhodecode.lib.profiler import ProfilingMiddleware
51 from rhodecode.lib.profiler import ProfilingMiddleware
52 app = ProfilingMiddleware(app)
52 app = ProfilingMiddleware(app)
53
53
54 app = SimpleHg(app, config)
55 app = SimpleGit(app, config)
56
57 if asbool(full_stack):
54 if asbool(full_stack):
58 # Handle Python exceptions
55 # Handle Python exceptions
59 app = ErrorHandler(app, global_conf, **config['pylons.errorware'])
56 app = ErrorHandler(app, global_conf, **config['pylons.errorware'])
60
57
61 # Display error documents for 401, 403, 404 status codes (and
58 # Display error documents for 401, 403, 404 status codes (and
62 # 500 when debug is disabled)
59 # 500 when debug is disabled)
63 if asbool(config['debug']):
60 if asbool(config['debug']):
64 app = StatusCodeRedirect(app)
61 app = StatusCodeRedirect(app)
65 else:
62 else:
66 app = StatusCodeRedirect(app, [400, 401, 403, 404, 500])
63 app = StatusCodeRedirect(app, [400, 401, 403, 404, 500])
67
64
68 #enable https redirets based on HTTP_X_URL_SCHEME set by proxy
65 #enable https redirets based on HTTP_X_URL_SCHEME set by proxy
69 app = HttpsFixup(app, config)
66 app = HttpsFixup(app, config)
70
67
71 # Establish the Registry for this application
68 # Establish the Registry for this application
72 app = RegistryManager(app)
69 app = RegistryManager(app)
73
70
74 if asbool(static_files):
71 if asbool(static_files):
75 # Serve static files
72 # Serve static files
76 static_app = StaticURLParser(config['pylons.paths']['static_files'])
73 static_app = StaticURLParser(config['pylons.paths']['static_files'])
77 app = Cascade([static_app, app])
74 app = Cascade([static_app, app])
78 app = make_gzip_middleware(app, global_conf, compress_level=1)
75 app = make_gzip_middleware(app, global_conf, compress_level=1)
79
76
77 # we want our low level middleware to get to the request ASAP. We don't
78 # need any pylons stack middleware in them
79 app = SimpleHg(app, config)
80 app = SimpleGit(app, config)
81
80 app.config = config
82 app.config = config
81
83
82 return app
84 return app
@@ -1,276 +1,289
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) 2009-2010 Marcin Kuzminski <marcin@python-works.com>
11 :copyright: (C) 2009-2010 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
30
31 from dulwich import server as dulserver
31 from dulwich import server as dulserver
32
32
33
33
34 class SimpleGitUploadPackHandler(dulserver.UploadPackHandler):
34 class SimpleGitUploadPackHandler(dulserver.UploadPackHandler):
35
35
36 def handle(self):
36 def handle(self):
37 write = lambda x: self.proto.write_sideband(1, x)
37 write = lambda x: self.proto.write_sideband(1, x)
38
38
39 graph_walker = dulserver.ProtocolGraphWalker(self,
39 graph_walker = dulserver.ProtocolGraphWalker(self,
40 self.repo.object_store,
40 self.repo.object_store,
41 self.repo.get_peeled)
41 self.repo.get_peeled)
42 objects_iter = self.repo.fetch_objects(
42 objects_iter = self.repo.fetch_objects(
43 graph_walker.determine_wants, graph_walker, self.progress,
43 graph_walker.determine_wants, graph_walker, self.progress,
44 get_tagged=self.get_tagged)
44 get_tagged=self.get_tagged)
45
45
46 # Do they want any objects?
46 # Do they want any objects?
47 if len(objects_iter) == 0:
47 if objects_iter is None or len(objects_iter) == 0:
48 return
48 return
49
49
50 self.progress("counting objects: %d, done.\n" % len(objects_iter))
50 self.progress("counting objects: %d, done.\n" % len(objects_iter))
51 dulserver.write_pack_data(dulserver.ProtocolFile(None, write),
51 dulserver.write_pack_objects(dulserver.ProtocolFile(None, write),
52 objects_iter, len(objects_iter))
52 objects_iter, len(objects_iter))
53 messages = []
53 messages = []
54 messages.append('thank you for using rhodecode')
54 messages.append('thank you for using rhodecode')
55
55
56 for msg in messages:
56 for msg in messages:
57 self.progress(msg + "\n")
57 self.progress(msg + "\n")
58 # we are done
58 # we are done
59 self.proto.write("0000")
59 self.proto.write("0000")
60
60
61 dulserver.DEFAULT_HANDLERS = {
61 dulserver.DEFAULT_HANDLERS = {
62 'git-upload-pack': SimpleGitUploadPackHandler,
62 'git-upload-pack': SimpleGitUploadPackHandler,
63 'git-receive-pack': dulserver.ReceivePackHandler,
63 'git-receive-pack': dulserver.ReceivePackHandler,
64 }
64 }
65
65
66 from dulwich.repo import Repo
66 from dulwich.repo import Repo
67 from dulwich.web import HTTPGitApplication
67 from dulwich.web import HTTPGitApplication
68
68
69 from paste.auth.basic import AuthBasicAuthenticator
69 from paste.auth.basic import AuthBasicAuthenticator
70 from paste.httpheaders import REMOTE_USER, AUTH_TYPE
70 from paste.httpheaders import REMOTE_USER, AUTH_TYPE
71
71
72 from rhodecode.lib import safe_str
72 from rhodecode.lib import safe_str
73 from rhodecode.lib.auth import authfunc, HasPermissionAnyMiddleware
73 from rhodecode.lib.auth import authfunc, HasPermissionAnyMiddleware
74 from rhodecode.lib.utils import invalidate_cache, check_repo_fast
74 from rhodecode.lib.utils import invalidate_cache, check_repo_fast
75 from rhodecode.model.user import UserModel
75 from rhodecode.model.user import UserModel
76
76
77 from webob.exc import HTTPNotFound, HTTPForbidden, HTTPInternalServerError
77 from webob.exc import HTTPNotFound, HTTPForbidden, HTTPInternalServerError
78
78
79 log = logging.getLogger(__name__)
79 log = logging.getLogger(__name__)
80
80
81
81
82 def is_git(environ):
82 def is_git(environ):
83 """Returns True if request's target is git server.
83 """Returns True if request's target is git server.
84 ``HTTP_USER_AGENT`` would then have git client version given.
84 ``HTTP_USER_AGENT`` would then have git client version given.
85
85
86 :param environ:
86 :param environ:
87 """
87 """
88 http_user_agent = environ.get('HTTP_USER_AGENT')
88 http_user_agent = environ.get('HTTP_USER_AGENT')
89 if http_user_agent and http_user_agent.startswith('git'):
89 if http_user_agent and http_user_agent.startswith('git'):
90 return True
90 return True
91 return False
91 return False
92
92
93
93
94 class SimpleGit(object):
94 class SimpleGit(object):
95
95
96 def __init__(self, application, config):
96 def __init__(self, application, config):
97 self.application = application
97 self.application = application
98 self.config = config
98 self.config = config
99 #authenticate this git request using
99 # base path of repo locations
100 self.basepath = self.config['base_path']
101 #authenticate this mercurial request using authfunc
100 self.authenticate = AuthBasicAuthenticator('', authfunc)
102 self.authenticate = AuthBasicAuthenticator('', authfunc)
101 self.ipaddr = '0.0.0.0'
102 self.repo_name = None
103 self.username = None
104 self.action = None
105
103
106 def __call__(self, environ, start_response):
104 def __call__(self, environ, start_response):
107 if not is_git(environ):
105 if not is_git(environ):
108 return self.application(environ, start_response)
106 return self.application(environ, start_response)
109
107
110 proxy_key = 'HTTP_X_REAL_IP'
108 proxy_key = 'HTTP_X_REAL_IP'
111 def_key = 'REMOTE_ADDR'
109 def_key = 'REMOTE_ADDR'
112 self.ipaddr = environ.get(proxy_key, environ.get(def_key, '0.0.0.0'))
110 ipaddr = environ.get(proxy_key, environ.get(def_key, '0.0.0.0'))
111 username = None
113 # skip passing error to error controller
112 # skip passing error to error controller
114 environ['pylons.status_code_redirect'] = True
113 environ['pylons.status_code_redirect'] = True
115
114
116 #======================================================================
115 #======================================================================
116 # EXTRACT REPOSITORY NAME FROM ENV
117 #======================================================================
118 try:
119 repo_name = self.__get_repository(environ)
120 log.debug('Extracted repo name is %s' % repo_name)
121 except:
122 return HTTPInternalServerError()(environ, start_response)
123
124 #======================================================================
117 # GET ACTION PULL or PUSH
125 # GET ACTION PULL or PUSH
118 #======================================================================
126 #======================================================================
119 self.action = self.__get_action(environ)
127 action = self.__get_action(environ)
120 try:
121 #==================================================================
122 # GET REPOSITORY NAME
123 #==================================================================
124 self.repo_name = self.__get_repository(environ)
125 except:
126 return HTTPInternalServerError()(environ, start_response)
127
128
128 #======================================================================
129 #======================================================================
129 # CHECK ANONYMOUS PERMISSION
130 # CHECK ANONYMOUS PERMISSION
130 #======================================================================
131 #======================================================================
131 if self.action in ['pull', 'push']:
132 if action in ['pull', 'push']:
132 anonymous_user = self.__get_user('default')
133 anonymous_user = self.__get_user('default')
133 self.username = anonymous_user.username
134 username = anonymous_user.username
134 anonymous_perm = self.__check_permission(self.action,
135 anonymous_perm = self.__check_permission(action,
135 anonymous_user,
136 anonymous_user,
136 self.repo_name)
137 repo_name)
137
138
138 if anonymous_perm is not True or anonymous_user.active is False:
139 if anonymous_perm is not True or anonymous_user.active is False:
139 if anonymous_perm is not True:
140 if anonymous_perm is not True:
140 log.debug('Not enough credentials to access this '
141 log.debug('Not enough credentials to access this '
141 'repository as anonymous user')
142 'repository as anonymous user')
142 if anonymous_user.active is False:
143 if anonymous_user.active is False:
143 log.debug('Anonymous access is disabled, running '
144 log.debug('Anonymous access is disabled, running '
144 'authentication')
145 'authentication')
145 #==============================================================
146 #==============================================================
146 # DEFAULT PERM FAILED OR ANONYMOUS ACCESS IS DISABLED SO WE
147 # DEFAULT PERM FAILED OR ANONYMOUS ACCESS IS DISABLED SO WE
147 # NEED TO AUTHENTICATE AND ASK FOR AUTH USER PERMISSIONS
148 # NEED TO AUTHENTICATE AND ASK FOR AUTH USER PERMISSIONS
148 #==============================================================
149 #==============================================================
149
150
150 if not REMOTE_USER(environ):
151 if not REMOTE_USER(environ):
151 self.authenticate.realm = \
152 self.authenticate.realm = \
152 safe_str(self.config['rhodecode_realm'])
153 safe_str(self.config['rhodecode_realm'])
153 result = self.authenticate(environ)
154 result = self.authenticate(environ)
154 if isinstance(result, str):
155 if isinstance(result, str):
155 AUTH_TYPE.update(environ, 'basic')
156 AUTH_TYPE.update(environ, 'basic')
156 REMOTE_USER.update(environ, result)
157 REMOTE_USER.update(environ, result)
157 else:
158 else:
158 return result.wsgi_application(environ, start_response)
159 return result.wsgi_application(environ, start_response)
159
160
160 #==============================================================
161 #==============================================================
161 # CHECK PERMISSIONS FOR THIS REQUEST USING GIVEN USERNAME FROM
162 # CHECK PERMISSIONS FOR THIS REQUEST USING GIVEN USERNAME FROM
162 # BASIC AUTH
163 # BASIC AUTH
163 #==============================================================
164 #==============================================================
164
165
165 if self.action in ['pull', 'push']:
166 if action in ['pull', 'push']:
166 username = REMOTE_USER(environ)
167 username = REMOTE_USER(environ)
167 try:
168 try:
168 user = self.__get_user(username)
169 user = self.__get_user(username)
169 self.username = user.username
170 username = user.username
170 except:
171 except:
171 log.error(traceback.format_exc())
172 log.error(traceback.format_exc())
172 return HTTPInternalServerError()(environ,
173 return HTTPInternalServerError()(environ,
173 start_response)
174 start_response)
174
175
175 #check permissions for this repository
176 #check permissions for this repository
176 perm = self.__check_permission(self.action, user,
177 perm = self.__check_permission(action, user,
177 self.repo_name)
178 repo_name)
178 if perm is not True:
179 if perm is not True:
179 return HTTPForbidden()(environ, start_response)
180 return HTTPForbidden()(environ, start_response)
180
181
181 self.extras = {'ip': self.ipaddr,
182 extras = {'ip': ipaddr,
182 'username': self.username,
183 'username': username,
183 'action': self.action,
184 'action': action,
184 'repository': self.repo_name}
185 'repository': repo_name}
185
186
186 #===================================================================
187 #===================================================================
187 # GIT REQUEST HANDLING
188 # GIT REQUEST HANDLING
188 #===================================================================
189 #===================================================================
189 self.basepath = self.config['base_path']
190
190 self.repo_path = os.path.join(self.basepath, self.repo_name)
191 repo_path = safe_str(os.path.join(self.basepath, repo_name))
192 log.debug('Repository path is %s' % repo_path)
193
191 #quick check if that dir exists...
194 # quick check if that dir exists...
192 if check_repo_fast(self.repo_name, self.basepath):
195 if check_repo_fast(repo_name, self.basepath):
193 return HTTPNotFound()(environ, start_response)
196 return HTTPNotFound()(environ, start_response)
197
194 try:
198 try:
195 app = self.__make_app()
199 #invalidate cache on push
196 except:
200 if action == 'push':
201 self.__invalidate_cache(repo_name)
202
203 app = self.__make_app(repo_name, repo_path)
204 return app(environ, start_response)
205 except Exception:
197 log.error(traceback.format_exc())
206 log.error(traceback.format_exc())
198 return HTTPInternalServerError()(environ, start_response)
207 return HTTPInternalServerError()(environ, start_response)
199
208
200 #invalidate cache on push
209 def __make_app(self, repo_name, repo_path):
201 if self.action == 'push':
210 """
202 self.__invalidate_cache(self.repo_name)
211 Make an wsgi application using dulserver
203
212
204 return app(environ, start_response)
213 :param repo_name: name of the repository
214 :param repo_path: full path to the repository
215 """
205
216
206 def __make_app(self):
217 _d = {'/' + repo_name: Repo(repo_path)}
207 _d = {'/' + self.repo_name: Repo(self.repo_path)}
208 backend = dulserver.DictBackend(_d)
218 backend = dulserver.DictBackend(_d)
209 gitserve = HTTPGitApplication(backend)
219 gitserve = HTTPGitApplication(backend)
210
220
211 return gitserve
221 return gitserve
212
222
213 def __check_permission(self, action, user, repo_name):
223 def __check_permission(self, action, user, repo_name):
214 """Checks permissions using action (push/pull) user and repository
224 """
225 Checks permissions using action (push/pull) user and repository
215 name
226 name
216
227
217 :param action: push or pull action
228 :param action: push or pull action
218 :param user: user instance
229 :param user: user instance
219 :param repo_name: repository name
230 :param repo_name: repository name
220 """
231 """
221 if action == 'push':
232 if action == 'push':
222 if not HasPermissionAnyMiddleware('repository.write',
233 if not HasPermissionAnyMiddleware('repository.write',
223 'repository.admin')(user,
234 'repository.admin')(user,
224 repo_name):
235 repo_name):
225 return False
236 return False
226
237
227 else:
238 else:
228 #any other action need at least read permission
239 #any other action need at least read permission
229 if not HasPermissionAnyMiddleware('repository.read',
240 if not HasPermissionAnyMiddleware('repository.read',
230 'repository.write',
241 'repository.write',
231 'repository.admin')(user,
242 'repository.admin')(user,
232 repo_name):
243 repo_name):
233 return False
244 return False
234
245
235 return True
246 return True
236
247
237 def __get_repository(self, environ):
248 def __get_repository(self, environ):
238 """Get's repository name out of PATH_INFO header
249 """
250 Get's repository name out of PATH_INFO header
239
251
240 :param environ: environ where PATH_INFO is stored
252 :param environ: environ where PATH_INFO is stored
241 """
253 """
242 try:
254 try:
243 repo_name = '/'.join(environ['PATH_INFO'].split('/')[1:])
255 repo_name = '/'.join(environ['PATH_INFO'].split('/')[1:])
244 if repo_name.endswith('/'):
256 if repo_name.endswith('/'):
245 repo_name = repo_name.rstrip('/')
257 repo_name = repo_name.rstrip('/')
246 except:
258 except:
247 log.error(traceback.format_exc())
259 log.error(traceback.format_exc())
248 raise
260 raise
249 repo_name = repo_name.split('/')[0]
261 repo_name = repo_name.split('/')[0]
250 return repo_name
262 return repo_name
251
263
252 def __get_user(self, username):
264 def __get_user(self, username):
253 return UserModel().get_by_username(username, cache=True)
265 return UserModel().get_by_username(username, cache=True)
254
266
255 def __get_action(self, environ):
267 def __get_action(self, environ):
256 """Maps git request commands into a pull or push command.
268 """Maps git request commands into a pull or push command.
257
269
258 :param environ:
270 :param environ:
259 """
271 """
260 service = environ['QUERY_STRING'].split('=')
272 service = environ['QUERY_STRING'].split('=')
261 if len(service) > 1:
273 if len(service) > 1:
262 service_cmd = service[1]
274 service_cmd = service[1]
263 mapping = {'git-receive-pack': 'push',
275 mapping = {'git-receive-pack': 'push',
264 'git-upload-pack': 'pull',
276 'git-upload-pack': 'pull',
265 }
277 }
266
278
267 return mapping.get(service_cmd,
279 return mapping.get(service_cmd,
268 service_cmd if service_cmd else 'other')
280 service_cmd if service_cmd else 'other')
269 else:
281 else:
270 return 'other'
282 return 'other'
271
283
272 def __invalidate_cache(self, repo_name):
284 def __invalidate_cache(self, repo_name):
273 """we know that some change was made to repositories and we should
285 """we know that some change was made to repositories and we should
274 invalidate the cache to see the changes right away but only for
286 invalidate the cache to see the changes right away but only for
275 push requests"""
287 push requests"""
276 invalidate_cache('get_repo_cached_%s' % repo_name)
288 invalidate_cache('get_repo_cached_%s' % repo_name)
289
General Comments 0
You need to be logged in to leave comments. Login now