##// END OF EJS Templates
mercurial middleware now returns 500's instead of 404 on errors and 404 when repo not found, added tracebacks
marcink -
r334:6c23e724 default
parent child Browse files
Show More
@@ -1,202 +1,213 b''
1 #!/usr/bin/env python
1 #!/usr/bin/env python
2 # encoding: utf-8
2 # encoding: utf-8
3 # middleware to handle mercurial api calls
3 # middleware to handle mercurial api calls
4 # Copyright (C) 2009-2010 Marcin Kuzminski <marcin@python-works.com>
4 # Copyright (C) 2009-2010 Marcin Kuzminski <marcin@python-works.com>
5 from mercurial.error import RepoError
5
6
6 # This program is free software; you can redistribute it and/or
7 # This program is free software; you can redistribute it and/or
7 # modify it under the terms of the GNU General Public License
8 # modify it under the terms of the GNU General Public License
8 # as published by the Free Software Foundation; version 2
9 # as published by the Free Software Foundation; version 2
9 # of the License or (at your opinion) any later version of the license.
10 # of the License or (at your opinion) any later version of the license.
10 #
11 #
11 # This program is distributed in the hope that it will be useful,
12 # This program is distributed in the hope that it will be useful,
12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
13 # but WITHOUT ANY WARRANTY; without even the implied warranty of
13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 # GNU General Public License for more details.
15 # GNU General Public License for more details.
15 #
16 #
16 # You should have received a copy of the GNU General Public License
17 # You should have received a copy of the GNU General Public License
17 # along with this program; if not, write to the Free Software
18 # along with this program; if not, write to the Free Software
18 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
19 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
19 # MA 02110-1301, USA.
20 # MA 02110-1301, USA.
20
21
21 """
22 """
22 Created on 2010-04-28
23 Created on 2010-04-28
23
24
24 @author: marcink
25 @author: marcink
25 SimpleHG middleware for handling mercurial protocol request (push/clone etc.)
26 SimpleHG middleware for handling mercurial protocol request (push/clone etc.)
26 It's implemented with basic auth function
27 It's implemented with basic auth function
27 """
28 """
28 from datetime import datetime
29 from datetime import datetime
29 from itertools import chain
30 from itertools import chain
30 from mercurial.hgweb import hgweb
31 from mercurial.hgweb import hgweb
31 from mercurial.hgweb.request import wsgiapplication
32 from mercurial.hgweb.request import wsgiapplication
32 from paste.auth.basic import AuthBasicAuthenticator
33 from paste.auth.basic import AuthBasicAuthenticator
33 from paste.httpheaders import REMOTE_USER, AUTH_TYPE
34 from paste.httpheaders import REMOTE_USER, AUTH_TYPE
34 from pylons_app.lib.auth import authfunc, HasPermissionAnyMiddleware
35 from pylons_app.lib.auth import authfunc, HasPermissionAnyMiddleware
35 from pylons_app.lib.utils import is_mercurial, make_ui, invalidate_cache
36 from pylons_app.lib.utils import is_mercurial, make_ui, invalidate_cache, \
37 check_repo_fast
36 from pylons_app.model import meta
38 from pylons_app.model import meta
37 from pylons_app.model.db import UserLog, User
39 from pylons_app.model.db import UserLog, User
38 from webob.exc import HTTPNotFound, HTTPForbidden
40 from webob.exc import HTTPNotFound, HTTPForbidden, HTTPInternalServerError
39 import logging
41 import logging
40 import os
42 import os
41 import traceback
43 import traceback
42 log = logging.getLogger(__name__)
44 log = logging.getLogger(__name__)
43
45
44 class SimpleHg(object):
46 class SimpleHg(object):
45
47
46 def __init__(self, application, config):
48 def __init__(self, application, config):
47 self.application = application
49 self.application = application
48 self.config = config
50 self.config = config
49 #authenticate this mercurial request using
51 #authenticate this mercurial request using
50 realm = '%s %s' % (self.config['hg_app_name'], 'mercurial repository')
52 realm = '%s %s' % (self.config['hg_app_name'], 'mercurial repository')
51 self.authenticate = AuthBasicAuthenticator(realm, authfunc)
53 self.authenticate = AuthBasicAuthenticator(realm, authfunc)
52
54
53 def __call__(self, environ, start_response):
55 def __call__(self, environ, start_response):
54 if not is_mercurial(environ):
56 if not is_mercurial(environ):
55 return self.application(environ, start_response)
57 return self.application(environ, start_response)
56 else:
58 else:
57 #===================================================================
59 #===================================================================
58 # AUTHENTICATE THIS MERCURIAL REQUEST
60 # AUTHENTICATE THIS MERCURIAL REQUEST
59 #===================================================================
61 #===================================================================
60 username = REMOTE_USER(environ)
62 username = REMOTE_USER(environ)
61 if not username:
63 if not username:
62 result = self.authenticate(environ)
64 result = self.authenticate(environ)
63 if isinstance(result, str):
65 if isinstance(result, str):
64 AUTH_TYPE.update(environ, 'basic')
66 AUTH_TYPE.update(environ, 'basic')
65 REMOTE_USER.update(environ, result)
67 REMOTE_USER.update(environ, result)
66 else:
68 else:
67 return result.wsgi_application(environ, start_response)
69 return result.wsgi_application(environ, start_response)
68
70
69 try:
71 try:
70 repo_name = '/'.join(environ['PATH_INFO'].split('/')[1:])
72 repo_name = '/'.join(environ['PATH_INFO'].split('/')[1:])
71 except Exception as e:
73 except:
72 log.error(traceback.format_exc())
74 log.error(traceback.format_exc())
73 return HTTPNotFound()(environ, start_response)
75 return HTTPInternalServerError()(environ, start_response)
74
76
75 #===================================================================
77 #===================================================================
76 # CHECK PERMISSIONS FOR THIS REQUEST
78 # CHECK PERMISSIONS FOR THIS REQUEST
77 #===================================================================
79 #===================================================================
78 action = self.__get_action(environ)
80 action = self.__get_action(environ)
79 if action:
81 if action:
80 username = self.__get_environ_user(environ)
82 username = self.__get_environ_user(environ)
81 try:
83 try:
82 sa = meta.Session
84 sa = meta.Session
83 user = sa.query(User)\
85 user = sa.query(User)\
84 .filter(User.username == username).one()
86 .filter(User.username == username).one()
85 except:
87 except:
86 return HTTPNotFound()(environ, start_response)
88 log.error(traceback.format_exc())
89 return HTTPInternalServerError()(environ, start_response)
87 #check permissions for this repository
90 #check permissions for this repository
88 if action == 'pull':
91 if action == 'pull':
89 if not HasPermissionAnyMiddleware('repository.read',
92 if not HasPermissionAnyMiddleware('repository.read',
90 'repository.write',
93 'repository.write',
91 'repository.admin')\
94 'repository.admin')\
92 (user, repo_name):
95 (user, repo_name):
93 return HTTPForbidden()(environ, start_response)
96 return HTTPForbidden()(environ, start_response)
94 if action == 'push':
97 if action == 'push':
95 if not HasPermissionAnyMiddleware('repository.write',
98 if not HasPermissionAnyMiddleware('repository.write',
96 'repository.admin')\
99 'repository.admin')\
97 (user, repo_name):
100 (user, repo_name):
98 return HTTPForbidden()(environ, start_response)
101 return HTTPForbidden()(environ, start_response)
99
102
100 #log action
103 #log action
101 proxy_key = 'HTTP_X_REAL_IP'
104 proxy_key = 'HTTP_X_REAL_IP'
102 def_key = 'REMOTE_ADDR'
105 def_key = 'REMOTE_ADDR'
103 ipaddr = environ.get(proxy_key, environ.get(def_key, '0.0.0.0'))
106 ipaddr = environ.get(proxy_key, environ.get(def_key, '0.0.0.0'))
104 self.__log_user_action(user, action, repo_name, ipaddr)
107 self.__log_user_action(user, action, repo_name, ipaddr)
105
108
106 #===================================================================
109 #===================================================================
107 # MERCURIAL REQUEST HANDLING
110 # MERCURIAL REQUEST HANDLING
108 #===================================================================
111 #===================================================================
109 environ['PATH_INFO'] = '/'#since we wrap into hgweb, reset the path
112 environ['PATH_INFO'] = '/'#since we wrap into hgweb, reset the path
110 self.baseui = make_ui(self.config['hg_app_repo_conf'])
113 self.baseui = make_ui(self.config['hg_app_repo_conf'])
111 self.basepath = self.config['base_path']
114 self.basepath = self.config['base_path']
112 self.repo_path = os.path.join(self.basepath, repo_name)
115 self.repo_path = os.path.join(self.basepath, repo_name)
116
117 #quick check if that dir exists...
118 if check_repo_fast(repo_name, self.basepath):
119 return HTTPNotFound()(environ, start_response)
120
113 try:
121 try:
114 app = wsgiapplication(self.__make_app)
122 app = wsgiapplication(self.__make_app)
123 except RepoError as e:
124 if str(e).find('not found') != -1:
125 return HTTPNotFound()(environ, start_response)
115 except Exception:
126 except Exception:
116 log.error(traceback.format_exc())
127 log.error(traceback.format_exc())
117 return HTTPNotFound()(environ, start_response)
128 return HTTPInternalServerError()(environ, start_response)
118
129
119
130
120 #invalidate cache on push
131 #invalidate cache on push
121 if action == 'push':
132 if action == 'push':
122 self.__invalidate_cache(repo_name)
133 self.__invalidate_cache(repo_name)
123
134
124 messages = ['thanks for using hg app !']
135 messages = ['thanks for using hg app !']
125 return self.msg_wrapper(app, environ, start_response, messages)
136 return self.msg_wrapper(app, environ, start_response, messages)
126
137
127
138
128 def msg_wrapper(self, app, environ, start_response, messages):
139 def msg_wrapper(self, app, environ, start_response, messages):
129 """
140 """
130 Wrapper for custom messages that come out of mercurial respond messages
141 Wrapper for custom messages that come out of mercurial respond messages
131 is a list of messages that the user will see at the end of response
142 is a list of messages that the user will see at the end of response
132 from merurial protocol actions that involves remote answers
143 from merurial protocol actions that involves remote answers
133 @param app:
144 @param app:
134 @param environ:
145 @param environ:
135 @param start_response:
146 @param start_response:
136 """
147 """
137 def custom_messages(msg_list):
148 def custom_messages(msg_list):
138 for msg in msg_list:
149 for msg in msg_list:
139 yield msg + '\n'
150 yield msg + '\n'
140 org_response = app(environ, start_response)
151 org_response = app(environ, start_response)
141 return chain(org_response, custom_messages(messages))
152 return chain(org_response, custom_messages(messages))
142
153
143 def __make_app(self):
154 def __make_app(self):
144 hgserve = hgweb(self.repo_path)
155 hgserve = hgweb(self.repo_path)
145 return self.__load_web_settings(hgserve)
156 return self.__load_web_settings(hgserve)
146
157
147 def __get_environ_user(self, environ):
158 def __get_environ_user(self, environ):
148 return environ.get('REMOTE_USER')
159 return environ.get('REMOTE_USER')
149
160
150 def __get_action(self, environ):
161 def __get_action(self, environ):
151 """
162 """
152 Maps mercurial request commands into a pull or push command.
163 Maps mercurial request commands into a pull or push command.
153 @param environ:
164 @param environ:
154 """
165 """
155 mapping = {'changegroup': 'pull',
166 mapping = {'changegroup': 'pull',
156 'changegroupsubset': 'pull',
167 'changegroupsubset': 'pull',
157 'stream_out': 'pull',
168 'stream_out': 'pull',
158 'listkeys': 'pull',
169 'listkeys': 'pull',
159 'unbundle': 'push',
170 'unbundle': 'push',
160 'pushkey': 'push', }
171 'pushkey': 'push', }
161
172
162 for qry in environ['QUERY_STRING'].split('&'):
173 for qry in environ['QUERY_STRING'].split('&'):
163 if qry.startswith('cmd'):
174 if qry.startswith('cmd'):
164 cmd = qry.split('=')[-1]
175 cmd = qry.split('=')[-1]
165 if mapping.has_key(cmd):
176 if mapping.has_key(cmd):
166 return mapping[cmd]
177 return mapping[cmd]
167
178
168 def __log_user_action(self, user, action, repo, ipaddr):
179 def __log_user_action(self, user, action, repo, ipaddr):
169 sa = meta.Session
180 sa = meta.Session
170 try:
181 try:
171 user_log = UserLog()
182 user_log = UserLog()
172 user_log.user_id = user.user_id
183 user_log.user_id = user.user_id
173 user_log.action = action
184 user_log.action = action
174 user_log.repository = repo.replace('/', '')
185 user_log.repository = repo.replace('/', '')
175 user_log.action_date = datetime.now()
186 user_log.action_date = datetime.now()
176 user_log.user_ip = ipaddr
187 user_log.user_ip = ipaddr
177 sa.add(user_log)
188 sa.add(user_log)
178 sa.commit()
189 sa.commit()
179 log.info('Adding user %s, action %s on %s',
190 log.info('Adding user %s, action %s on %s',
180 user.username, action, repo)
191 user.username, action, repo)
181 except Exception as e:
192 except Exception as e:
182 sa.rollback()
193 sa.rollback()
183 log.error('could not log user action:%s', str(e))
194 log.error('could not log user action:%s', str(e))
184
195
185 def __invalidate_cache(self, repo_name):
196 def __invalidate_cache(self, repo_name):
186 """we know that some change was made to repositories and we should
197 """we know that some change was made to repositories and we should
187 invalidate the cache to see the changes right away but only for
198 invalidate the cache to see the changes right away but only for
188 push requests"""
199 push requests"""
189 invalidate_cache('cached_repo_list')
200 invalidate_cache('cached_repo_list')
190 invalidate_cache('full_changelog', repo_name)
201 invalidate_cache('full_changelog', repo_name)
191
202
192
203
193 def __load_web_settings(self, hgserve):
204 def __load_web_settings(self, hgserve):
194 repoui = make_ui(os.path.join(self.repo_path, '.hg', 'hgrc'), False)
205 repoui = make_ui(os.path.join(self.repo_path, '.hg', 'hgrc'), False)
195 #set the global ui for hgserve
206 #set the global ui for hgserve
196 hgserve.repo.ui = self.baseui
207 hgserve.repo.ui = self.baseui
197
208
198 if repoui:
209 if repoui:
199 #set the repository based config
210 #set the repository based config
200 hgserve.repo.ui = repoui
211 hgserve.repo.ui = repoui
201
212
202 return hgserve
213 return hgserve
General Comments 0
You need to be logged in to leave comments. Login now