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