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