##// END OF EJS Templates
python3: fixed urllib usage
super-admin -
r4950:e4140a41 default
parent child Browse files
Show More
@@ -1,470 +1,470 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2016-2020 RhodeCode GmbH
3 # Copyright (C) 2016-2020 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21 import time
21 import time
22 import collections
22 import collections
23 import datetime
23 import datetime
24 import formencode
24 import formencode
25 import formencode.htmlfill
25 import formencode.htmlfill
26 import logging
26 import logging
27 import urllib.parse
27 import urllib.parse
28 import requests
28 import requests
29
29
30 from pyramid.httpexceptions import HTTPFound
30 from pyramid.httpexceptions import HTTPFound
31
31
32
32
33 from rhodecode.apps._base import BaseAppView
33 from rhodecode.apps._base import BaseAppView
34 from rhodecode.authentication.base import authenticate, HTTP_TYPE
34 from rhodecode.authentication.base import authenticate, HTTP_TYPE
35 from rhodecode.authentication.plugins import auth_rhodecode
35 from rhodecode.authentication.plugins import auth_rhodecode
36 from rhodecode.events import UserRegistered, trigger
36 from rhodecode.events import UserRegistered, trigger
37 from rhodecode.lib import helpers as h
37 from rhodecode.lib import helpers as h
38 from rhodecode.lib import audit_logger
38 from rhodecode.lib import audit_logger
39 from rhodecode.lib.auth import (
39 from rhodecode.lib.auth import (
40 AuthUser, HasPermissionAnyDecorator, CSRFRequired)
40 AuthUser, HasPermissionAnyDecorator, CSRFRequired)
41 from rhodecode.lib.base import get_ip_addr
41 from rhodecode.lib.base import get_ip_addr
42 from rhodecode.lib.exceptions import UserCreationError
42 from rhodecode.lib.exceptions import UserCreationError
43 from rhodecode.lib.utils2 import safe_str
43 from rhodecode.lib.utils2 import safe_str
44 from rhodecode.model.db import User, UserApiKeys
44 from rhodecode.model.db import User, UserApiKeys
45 from rhodecode.model.forms import LoginForm, RegisterForm, PasswordResetForm
45 from rhodecode.model.forms import LoginForm, RegisterForm, PasswordResetForm
46 from rhodecode.model.meta import Session
46 from rhodecode.model.meta import Session
47 from rhodecode.model.auth_token import AuthTokenModel
47 from rhodecode.model.auth_token import AuthTokenModel
48 from rhodecode.model.settings import SettingsModel
48 from rhodecode.model.settings import SettingsModel
49 from rhodecode.model.user import UserModel
49 from rhodecode.model.user import UserModel
50 from rhodecode.translation import _
50 from rhodecode.translation import _
51
51
52
52
53 log = logging.getLogger(__name__)
53 log = logging.getLogger(__name__)
54
54
55 CaptchaData = collections.namedtuple(
55 CaptchaData = collections.namedtuple(
56 'CaptchaData', 'active, private_key, public_key')
56 'CaptchaData', 'active, private_key, public_key')
57
57
58
58
59 def store_user_in_session(session, username, remember=False):
59 def store_user_in_session(session, username, remember=False):
60 user = User.get_by_username(username, case_insensitive=True)
60 user = User.get_by_username(username, case_insensitive=True)
61 auth_user = AuthUser(user.user_id)
61 auth_user = AuthUser(user.user_id)
62 auth_user.set_authenticated()
62 auth_user.set_authenticated()
63 cs = auth_user.get_cookie_store()
63 cs = auth_user.get_cookie_store()
64 session['rhodecode_user'] = cs
64 session['rhodecode_user'] = cs
65 user.update_lastlogin()
65 user.update_lastlogin()
66 Session().commit()
66 Session().commit()
67
67
68 # If they want to be remembered, update the cookie
68 # If they want to be remembered, update the cookie
69 if remember:
69 if remember:
70 _year = (datetime.datetime.now() +
70 _year = (datetime.datetime.now() +
71 datetime.timedelta(seconds=60 * 60 * 24 * 365))
71 datetime.timedelta(seconds=60 * 60 * 24 * 365))
72 session._set_cookie_expires(_year)
72 session._set_cookie_expires(_year)
73
73
74 session.save()
74 session.save()
75
75
76 safe_cs = cs.copy()
76 safe_cs = cs.copy()
77 safe_cs['password'] = '****'
77 safe_cs['password'] = '****'
78 log.info('user %s is now authenticated and stored in '
78 log.info('user %s is now authenticated and stored in '
79 'session, session attrs %s', username, safe_cs)
79 'session, session attrs %s', username, safe_cs)
80
80
81 # dumps session attrs back to cookie
81 # dumps session attrs back to cookie
82 session._update_cookie_out()
82 session._update_cookie_out()
83 # we set new cookie
83 # we set new cookie
84 headers = None
84 headers = None
85 if session.request['set_cookie']:
85 if session.request['set_cookie']:
86 # send set-cookie headers back to response to update cookie
86 # send set-cookie headers back to response to update cookie
87 headers = [('Set-Cookie', session.request['cookie_out'])]
87 headers = [('Set-Cookie', session.request['cookie_out'])]
88 return headers
88 return headers
89
89
90
90
91 def get_came_from(request):
91 def get_came_from(request):
92 came_from = safe_str(request.GET.get('came_from', ''))
92 came_from = safe_str(request.GET.get('came_from', ''))
93 parsed = urllib.parse.urlparse.urlparse(came_from)
93 parsed = urllib.parse.urlparse(came_from)
94 allowed_schemes = ['http', 'https']
94 allowed_schemes = ['http', 'https']
95 default_came_from = h.route_path('home')
95 default_came_from = h.route_path('home')
96 if parsed.scheme and parsed.scheme not in allowed_schemes:
96 if parsed.scheme and parsed.scheme not in allowed_schemes:
97 log.error('Suspicious URL scheme detected %s for url %s',
97 log.error('Suspicious URL scheme detected %s for url %s',
98 parsed.scheme, parsed)
98 parsed.scheme, parsed)
99 came_from = default_came_from
99 came_from = default_came_from
100 elif parsed.netloc and request.host != parsed.netloc:
100 elif parsed.netloc and request.host != parsed.netloc:
101 log.error('Suspicious NETLOC detected %s for url %s server url '
101 log.error('Suspicious NETLOC detected %s for url %s server url '
102 'is: %s', parsed.netloc, parsed, request.host)
102 'is: %s', parsed.netloc, parsed, request.host)
103 came_from = default_came_from
103 came_from = default_came_from
104 elif any(bad_str in parsed.path for bad_str in ('\r', '\n')):
104 elif any(bad_str in parsed.path for bad_str in ('\r', '\n')):
105 log.error('Header injection detected `%s` for url %s server url ',
105 log.error('Header injection detected `%s` for url %s server url ',
106 parsed.path, parsed)
106 parsed.path, parsed)
107 came_from = default_came_from
107 came_from = default_came_from
108
108
109 return came_from or default_came_from
109 return came_from or default_came_from
110
110
111
111
112 class LoginView(BaseAppView):
112 class LoginView(BaseAppView):
113
113
114 def load_default_context(self):
114 def load_default_context(self):
115 c = self._get_local_tmpl_context()
115 c = self._get_local_tmpl_context()
116 c.came_from = get_came_from(self.request)
116 c.came_from = get_came_from(self.request)
117 return c
117 return c
118
118
119 def _get_captcha_data(self):
119 def _get_captcha_data(self):
120 settings = SettingsModel().get_all_settings()
120 settings = SettingsModel().get_all_settings()
121 private_key = settings.get('rhodecode_captcha_private_key')
121 private_key = settings.get('rhodecode_captcha_private_key')
122 public_key = settings.get('rhodecode_captcha_public_key')
122 public_key = settings.get('rhodecode_captcha_public_key')
123 active = bool(private_key)
123 active = bool(private_key)
124 return CaptchaData(
124 return CaptchaData(
125 active=active, private_key=private_key, public_key=public_key)
125 active=active, private_key=private_key, public_key=public_key)
126
126
127 def validate_captcha(self, private_key):
127 def validate_captcha(self, private_key):
128
128
129 captcha_rs = self.request.POST.get('g-recaptcha-response')
129 captcha_rs = self.request.POST.get('g-recaptcha-response')
130 url = "https://www.google.com/recaptcha/api/siteverify"
130 url = "https://www.google.com/recaptcha/api/siteverify"
131 params = {
131 params = {
132 'secret': private_key,
132 'secret': private_key,
133 'response': captcha_rs,
133 'response': captcha_rs,
134 'remoteip': get_ip_addr(self.request.environ)
134 'remoteip': get_ip_addr(self.request.environ)
135 }
135 }
136 verify_rs = requests.get(url, params=params, verify=True, timeout=60)
136 verify_rs = requests.get(url, params=params, verify=True, timeout=60)
137 verify_rs = verify_rs.json()
137 verify_rs = verify_rs.json()
138 captcha_status = verify_rs.get('success', False)
138 captcha_status = verify_rs.get('success', False)
139 captcha_errors = verify_rs.get('error-codes', [])
139 captcha_errors = verify_rs.get('error-codes', [])
140 if not isinstance(captcha_errors, list):
140 if not isinstance(captcha_errors, list):
141 captcha_errors = [captcha_errors]
141 captcha_errors = [captcha_errors]
142 captcha_errors = ', '.join(captcha_errors)
142 captcha_errors = ', '.join(captcha_errors)
143 captcha_message = ''
143 captcha_message = ''
144 if captcha_status is False:
144 if captcha_status is False:
145 captcha_message = "Bad captcha. Errors: {}".format(
145 captcha_message = "Bad captcha. Errors: {}".format(
146 captcha_errors)
146 captcha_errors)
147
147
148 return captcha_status, captcha_message
148 return captcha_status, captcha_message
149
149
150 def login(self):
150 def login(self):
151 c = self.load_default_context()
151 c = self.load_default_context()
152 auth_user = self._rhodecode_user
152 auth_user = self._rhodecode_user
153
153
154 # redirect if already logged in
154 # redirect if already logged in
155 if (auth_user.is_authenticated and
155 if (auth_user.is_authenticated and
156 not auth_user.is_default and auth_user.ip_allowed):
156 not auth_user.is_default and auth_user.ip_allowed):
157 raise HTTPFound(c.came_from)
157 raise HTTPFound(c.came_from)
158
158
159 # check if we use headers plugin, and try to login using it.
159 # check if we use headers plugin, and try to login using it.
160 try:
160 try:
161 log.debug('Running PRE-AUTH for headers based authentication')
161 log.debug('Running PRE-AUTH for headers based authentication')
162 auth_info = authenticate(
162 auth_info = authenticate(
163 '', '', self.request.environ, HTTP_TYPE, skip_missing=True)
163 '', '', self.request.environ, HTTP_TYPE, skip_missing=True)
164 if auth_info:
164 if auth_info:
165 headers = store_user_in_session(
165 headers = store_user_in_session(
166 self.session, auth_info.get('username'))
166 self.session, auth_info.get('username'))
167 raise HTTPFound(c.came_from, headers=headers)
167 raise HTTPFound(c.came_from, headers=headers)
168 except UserCreationError as e:
168 except UserCreationError as e:
169 log.error(e)
169 log.error(e)
170 h.flash(e, category='error')
170 h.flash(e, category='error')
171
171
172 return self._get_template_context(c)
172 return self._get_template_context(c)
173
173
174 def login_post(self):
174 def login_post(self):
175 c = self.load_default_context()
175 c = self.load_default_context()
176
176
177 login_form = LoginForm(self.request.translate)()
177 login_form = LoginForm(self.request.translate)()
178
178
179 try:
179 try:
180 self.session.invalidate()
180 self.session.invalidate()
181 form_result = login_form.to_python(self.request.POST)
181 form_result = login_form.to_python(self.request.POST)
182 # form checks for username/password, now we're authenticated
182 # form checks for username/password, now we're authenticated
183 headers = store_user_in_session(
183 headers = store_user_in_session(
184 self.session,
184 self.session,
185 username=form_result['username'],
185 username=form_result['username'],
186 remember=form_result['remember'])
186 remember=form_result['remember'])
187 log.debug('Redirecting to "%s" after login.', c.came_from)
187 log.debug('Redirecting to "%s" after login.', c.came_from)
188
188
189 audit_user = audit_logger.UserWrap(
189 audit_user = audit_logger.UserWrap(
190 username=self.request.POST.get('username'),
190 username=self.request.POST.get('username'),
191 ip_addr=self.request.remote_addr)
191 ip_addr=self.request.remote_addr)
192 action_data = {'user_agent': self.request.user_agent}
192 action_data = {'user_agent': self.request.user_agent}
193 audit_logger.store_web(
193 audit_logger.store_web(
194 'user.login.success', action_data=action_data,
194 'user.login.success', action_data=action_data,
195 user=audit_user, commit=True)
195 user=audit_user, commit=True)
196
196
197 raise HTTPFound(c.came_from, headers=headers)
197 raise HTTPFound(c.came_from, headers=headers)
198 except formencode.Invalid as errors:
198 except formencode.Invalid as errors:
199 defaults = errors.value
199 defaults = errors.value
200 # remove password from filling in form again
200 # remove password from filling in form again
201 defaults.pop('password', None)
201 defaults.pop('password', None)
202 render_ctx = {
202 render_ctx = {
203 'errors': errors.error_dict,
203 'errors': errors.error_dict,
204 'defaults': defaults,
204 'defaults': defaults,
205 }
205 }
206
206
207 audit_user = audit_logger.UserWrap(
207 audit_user = audit_logger.UserWrap(
208 username=self.request.POST.get('username'),
208 username=self.request.POST.get('username'),
209 ip_addr=self.request.remote_addr)
209 ip_addr=self.request.remote_addr)
210 action_data = {'user_agent': self.request.user_agent}
210 action_data = {'user_agent': self.request.user_agent}
211 audit_logger.store_web(
211 audit_logger.store_web(
212 'user.login.failure', action_data=action_data,
212 'user.login.failure', action_data=action_data,
213 user=audit_user, commit=True)
213 user=audit_user, commit=True)
214 return self._get_template_context(c, **render_ctx)
214 return self._get_template_context(c, **render_ctx)
215
215
216 except UserCreationError as e:
216 except UserCreationError as e:
217 # headers auth or other auth functions that create users on
217 # headers auth or other auth functions that create users on
218 # the fly can throw this exception signaling that there's issue
218 # the fly can throw this exception signaling that there's issue
219 # with user creation, explanation should be provided in
219 # with user creation, explanation should be provided in
220 # Exception itself
220 # Exception itself
221 h.flash(e, category='error')
221 h.flash(e, category='error')
222 return self._get_template_context(c)
222 return self._get_template_context(c)
223
223
224 @CSRFRequired()
224 @CSRFRequired()
225 def logout(self):
225 def logout(self):
226 auth_user = self._rhodecode_user
226 auth_user = self._rhodecode_user
227 log.info('Deleting session for user: `%s`', auth_user)
227 log.info('Deleting session for user: `%s`', auth_user)
228
228
229 action_data = {'user_agent': self.request.user_agent}
229 action_data = {'user_agent': self.request.user_agent}
230 audit_logger.store_web(
230 audit_logger.store_web(
231 'user.logout', action_data=action_data,
231 'user.logout', action_data=action_data,
232 user=auth_user, commit=True)
232 user=auth_user, commit=True)
233 self.session.delete()
233 self.session.delete()
234 return HTTPFound(h.route_path('home'))
234 return HTTPFound(h.route_path('home'))
235
235
236 @HasPermissionAnyDecorator(
236 @HasPermissionAnyDecorator(
237 'hg.admin', 'hg.register.auto_activate', 'hg.register.manual_activate')
237 'hg.admin', 'hg.register.auto_activate', 'hg.register.manual_activate')
238 def register(self, defaults=None, errors=None):
238 def register(self, defaults=None, errors=None):
239 c = self.load_default_context()
239 c = self.load_default_context()
240 defaults = defaults or {}
240 defaults = defaults or {}
241 errors = errors or {}
241 errors = errors or {}
242
242
243 settings = SettingsModel().get_all_settings()
243 settings = SettingsModel().get_all_settings()
244 register_message = settings.get('rhodecode_register_message') or ''
244 register_message = settings.get('rhodecode_register_message') or ''
245 captcha = self._get_captcha_data()
245 captcha = self._get_captcha_data()
246 auto_active = 'hg.register.auto_activate' in User.get_default_user()\
246 auto_active = 'hg.register.auto_activate' in User.get_default_user()\
247 .AuthUser().permissions['global']
247 .AuthUser().permissions['global']
248
248
249 render_ctx = self._get_template_context(c)
249 render_ctx = self._get_template_context(c)
250 render_ctx.update({
250 render_ctx.update({
251 'defaults': defaults,
251 'defaults': defaults,
252 'errors': errors,
252 'errors': errors,
253 'auto_active': auto_active,
253 'auto_active': auto_active,
254 'captcha_active': captcha.active,
254 'captcha_active': captcha.active,
255 'captcha_public_key': captcha.public_key,
255 'captcha_public_key': captcha.public_key,
256 'register_message': register_message,
256 'register_message': register_message,
257 })
257 })
258 return render_ctx
258 return render_ctx
259
259
260 @HasPermissionAnyDecorator(
260 @HasPermissionAnyDecorator(
261 'hg.admin', 'hg.register.auto_activate', 'hg.register.manual_activate')
261 'hg.admin', 'hg.register.auto_activate', 'hg.register.manual_activate')
262 def register_post(self):
262 def register_post(self):
263 from rhodecode.authentication.plugins import auth_rhodecode
263 from rhodecode.authentication.plugins import auth_rhodecode
264
264
265 self.load_default_context()
265 self.load_default_context()
266 captcha = self._get_captcha_data()
266 captcha = self._get_captcha_data()
267 auto_active = 'hg.register.auto_activate' in User.get_default_user()\
267 auto_active = 'hg.register.auto_activate' in User.get_default_user()\
268 .AuthUser().permissions['global']
268 .AuthUser().permissions['global']
269
269
270 extern_name = auth_rhodecode.RhodeCodeAuthPlugin.uid
270 extern_name = auth_rhodecode.RhodeCodeAuthPlugin.uid
271 extern_type = auth_rhodecode.RhodeCodeAuthPlugin.uid
271 extern_type = auth_rhodecode.RhodeCodeAuthPlugin.uid
272
272
273 register_form = RegisterForm(self.request.translate)()
273 register_form = RegisterForm(self.request.translate)()
274 try:
274 try:
275
275
276 form_result = register_form.to_python(self.request.POST)
276 form_result = register_form.to_python(self.request.POST)
277 form_result['active'] = auto_active
277 form_result['active'] = auto_active
278 external_identity = self.request.POST.get('external_identity')
278 external_identity = self.request.POST.get('external_identity')
279
279
280 if external_identity:
280 if external_identity:
281 extern_name = external_identity
281 extern_name = external_identity
282 extern_type = external_identity
282 extern_type = external_identity
283
283
284 if captcha.active:
284 if captcha.active:
285 captcha_status, captcha_message = self.validate_captcha(
285 captcha_status, captcha_message = self.validate_captcha(
286 captcha.private_key)
286 captcha.private_key)
287
287
288 if not captcha_status:
288 if not captcha_status:
289 _value = form_result
289 _value = form_result
290 _msg = _('Bad captcha')
290 _msg = _('Bad captcha')
291 error_dict = {'recaptcha_field': captcha_message}
291 error_dict = {'recaptcha_field': captcha_message}
292 raise formencode.Invalid(
292 raise formencode.Invalid(
293 _msg, _value, None, error_dict=error_dict)
293 _msg, _value, None, error_dict=error_dict)
294
294
295 new_user = UserModel().create_registration(
295 new_user = UserModel().create_registration(
296 form_result, extern_name=extern_name, extern_type=extern_type)
296 form_result, extern_name=extern_name, extern_type=extern_type)
297
297
298 action_data = {'data': new_user.get_api_data(),
298 action_data = {'data': new_user.get_api_data(),
299 'user_agent': self.request.user_agent}
299 'user_agent': self.request.user_agent}
300
300
301 if external_identity:
301 if external_identity:
302 action_data['external_identity'] = external_identity
302 action_data['external_identity'] = external_identity
303
303
304 audit_user = audit_logger.UserWrap(
304 audit_user = audit_logger.UserWrap(
305 username=new_user.username,
305 username=new_user.username,
306 user_id=new_user.user_id,
306 user_id=new_user.user_id,
307 ip_addr=self.request.remote_addr)
307 ip_addr=self.request.remote_addr)
308
308
309 audit_logger.store_web(
309 audit_logger.store_web(
310 'user.register', action_data=action_data,
310 'user.register', action_data=action_data,
311 user=audit_user)
311 user=audit_user)
312
312
313 event = UserRegistered(user=new_user, session=self.session)
313 event = UserRegistered(user=new_user, session=self.session)
314 trigger(event)
314 trigger(event)
315 h.flash(
315 h.flash(
316 _('You have successfully registered with RhodeCode. You can log-in now.'),
316 _('You have successfully registered with RhodeCode. You can log-in now.'),
317 category='success')
317 category='success')
318 if external_identity:
318 if external_identity:
319 h.flash(
319 h.flash(
320 _('Please use the {identity} button to log-in').format(
320 _('Please use the {identity} button to log-in').format(
321 identity=external_identity),
321 identity=external_identity),
322 category='success')
322 category='success')
323 Session().commit()
323 Session().commit()
324
324
325 redirect_ro = self.request.route_path('login')
325 redirect_ro = self.request.route_path('login')
326 raise HTTPFound(redirect_ro)
326 raise HTTPFound(redirect_ro)
327
327
328 except formencode.Invalid as errors:
328 except formencode.Invalid as errors:
329 errors.value.pop('password', None)
329 errors.value.pop('password', None)
330 errors.value.pop('password_confirmation', None)
330 errors.value.pop('password_confirmation', None)
331 return self.register(
331 return self.register(
332 defaults=errors.value, errors=errors.error_dict)
332 defaults=errors.value, errors=errors.error_dict)
333
333
334 except UserCreationError as e:
334 except UserCreationError as e:
335 # container auth or other auth functions that create users on
335 # container auth or other auth functions that create users on
336 # the fly can throw this exception signaling that there's issue
336 # the fly can throw this exception signaling that there's issue
337 # with user creation, explanation should be provided in
337 # with user creation, explanation should be provided in
338 # Exception itself
338 # Exception itself
339 h.flash(e, category='error')
339 h.flash(e, category='error')
340 return self.register()
340 return self.register()
341
341
342 def password_reset(self):
342 def password_reset(self):
343 c = self.load_default_context()
343 c = self.load_default_context()
344 captcha = self._get_captcha_data()
344 captcha = self._get_captcha_data()
345
345
346 template_context = {
346 template_context = {
347 'captcha_active': captcha.active,
347 'captcha_active': captcha.active,
348 'captcha_public_key': captcha.public_key,
348 'captcha_public_key': captcha.public_key,
349 'defaults': {},
349 'defaults': {},
350 'errors': {},
350 'errors': {},
351 }
351 }
352
352
353 # always send implicit message to prevent from discovery of
353 # always send implicit message to prevent from discovery of
354 # matching emails
354 # matching emails
355 msg = _('If such email exists, a password reset link was sent to it.')
355 msg = _('If such email exists, a password reset link was sent to it.')
356
356
357 def default_response():
357 def default_response():
358 log.debug('faking response on invalid password reset')
358 log.debug('faking response on invalid password reset')
359 # make this take 2s, to prevent brute forcing.
359 # make this take 2s, to prevent brute forcing.
360 time.sleep(2)
360 time.sleep(2)
361 h.flash(msg, category='success')
361 h.flash(msg, category='success')
362 return HTTPFound(self.request.route_path('reset_password'))
362 return HTTPFound(self.request.route_path('reset_password'))
363
363
364 if self.request.POST:
364 if self.request.POST:
365 if h.HasPermissionAny('hg.password_reset.disabled')():
365 if h.HasPermissionAny('hg.password_reset.disabled')():
366 _email = self.request.POST.get('email', '')
366 _email = self.request.POST.get('email', '')
367 log.error('Failed attempt to reset password for `%s`.', _email)
367 log.error('Failed attempt to reset password for `%s`.', _email)
368 h.flash(_('Password reset has been disabled.'), category='error')
368 h.flash(_('Password reset has been disabled.'), category='error')
369 return HTTPFound(self.request.route_path('reset_password'))
369 return HTTPFound(self.request.route_path('reset_password'))
370
370
371 password_reset_form = PasswordResetForm(self.request.translate)()
371 password_reset_form = PasswordResetForm(self.request.translate)()
372 description = u'Generated token for password reset from {}'.format(
372 description = u'Generated token for password reset from {}'.format(
373 datetime.datetime.now().isoformat())
373 datetime.datetime.now().isoformat())
374
374
375 try:
375 try:
376 form_result = password_reset_form.to_python(
376 form_result = password_reset_form.to_python(
377 self.request.POST)
377 self.request.POST)
378 user_email = form_result['email']
378 user_email = form_result['email']
379
379
380 if captcha.active:
380 if captcha.active:
381 captcha_status, captcha_message = self.validate_captcha(
381 captcha_status, captcha_message = self.validate_captcha(
382 captcha.private_key)
382 captcha.private_key)
383
383
384 if not captcha_status:
384 if not captcha_status:
385 _value = form_result
385 _value = form_result
386 _msg = _('Bad captcha')
386 _msg = _('Bad captcha')
387 error_dict = {'recaptcha_field': captcha_message}
387 error_dict = {'recaptcha_field': captcha_message}
388 raise formencode.Invalid(
388 raise formencode.Invalid(
389 _msg, _value, None, error_dict=error_dict)
389 _msg, _value, None, error_dict=error_dict)
390
390
391 # Generate reset URL and send mail.
391 # Generate reset URL and send mail.
392 user = User.get_by_email(user_email)
392 user = User.get_by_email(user_email)
393
393
394 # only allow rhodecode based users to reset their password
394 # only allow rhodecode based users to reset their password
395 # external auth shouldn't allow password reset
395 # external auth shouldn't allow password reset
396 if user and user.extern_type != auth_rhodecode.RhodeCodeAuthPlugin.uid:
396 if user and user.extern_type != auth_rhodecode.RhodeCodeAuthPlugin.uid:
397 log.warning('User %s with external type `%s` tried a password reset. '
397 log.warning('User %s with external type `%s` tried a password reset. '
398 'This try was rejected', user, user.extern_type)
398 'This try was rejected', user, user.extern_type)
399 return default_response()
399 return default_response()
400
400
401 # generate password reset token that expires in 10 minutes
401 # generate password reset token that expires in 10 minutes
402 reset_token = UserModel().add_auth_token(
402 reset_token = UserModel().add_auth_token(
403 user=user, lifetime_minutes=10,
403 user=user, lifetime_minutes=10,
404 role=UserModel.auth_token_role.ROLE_PASSWORD_RESET,
404 role=UserModel.auth_token_role.ROLE_PASSWORD_RESET,
405 description=description)
405 description=description)
406 Session().commit()
406 Session().commit()
407
407
408 log.debug('Successfully created password recovery token')
408 log.debug('Successfully created password recovery token')
409 password_reset_url = self.request.route_url(
409 password_reset_url = self.request.route_url(
410 'reset_password_confirmation',
410 'reset_password_confirmation',
411 _query={'key': reset_token.api_key})
411 _query={'key': reset_token.api_key})
412 UserModel().reset_password_link(
412 UserModel().reset_password_link(
413 form_result, password_reset_url)
413 form_result, password_reset_url)
414
414
415 action_data = {'email': user_email,
415 action_data = {'email': user_email,
416 'user_agent': self.request.user_agent}
416 'user_agent': self.request.user_agent}
417 audit_logger.store_web(
417 audit_logger.store_web(
418 'user.password.reset_request', action_data=action_data,
418 'user.password.reset_request', action_data=action_data,
419 user=self._rhodecode_user, commit=True)
419 user=self._rhodecode_user, commit=True)
420
420
421 return default_response()
421 return default_response()
422
422
423 except formencode.Invalid as errors:
423 except formencode.Invalid as errors:
424 template_context.update({
424 template_context.update({
425 'defaults': errors.value,
425 'defaults': errors.value,
426 'errors': errors.error_dict,
426 'errors': errors.error_dict,
427 })
427 })
428 if not self.request.POST.get('email'):
428 if not self.request.POST.get('email'):
429 # case of empty email, we want to report that
429 # case of empty email, we want to report that
430 return self._get_template_context(c, **template_context)
430 return self._get_template_context(c, **template_context)
431
431
432 if 'recaptcha_field' in errors.error_dict:
432 if 'recaptcha_field' in errors.error_dict:
433 # case of failed captcha
433 # case of failed captcha
434 return self._get_template_context(c, **template_context)
434 return self._get_template_context(c, **template_context)
435
435
436 return default_response()
436 return default_response()
437
437
438 return self._get_template_context(c, **template_context)
438 return self._get_template_context(c, **template_context)
439
439
440 def password_reset_confirmation(self):
440 def password_reset_confirmation(self):
441 self.load_default_context()
441 self.load_default_context()
442 if self.request.GET and self.request.GET.get('key'):
442 if self.request.GET and self.request.GET.get('key'):
443 # make this take 2s, to prevent brute forcing.
443 # make this take 2s, to prevent brute forcing.
444 time.sleep(2)
444 time.sleep(2)
445
445
446 token = AuthTokenModel().get_auth_token(
446 token = AuthTokenModel().get_auth_token(
447 self.request.GET.get('key'))
447 self.request.GET.get('key'))
448
448
449 # verify token is the correct role
449 # verify token is the correct role
450 if token is None or token.role != UserApiKeys.ROLE_PASSWORD_RESET:
450 if token is None or token.role != UserApiKeys.ROLE_PASSWORD_RESET:
451 log.debug('Got token with role:%s expected is %s',
451 log.debug('Got token with role:%s expected is %s',
452 getattr(token, 'role', 'EMPTY_TOKEN'),
452 getattr(token, 'role', 'EMPTY_TOKEN'),
453 UserApiKeys.ROLE_PASSWORD_RESET)
453 UserApiKeys.ROLE_PASSWORD_RESET)
454 h.flash(
454 h.flash(
455 _('Given reset token is invalid'), category='error')
455 _('Given reset token is invalid'), category='error')
456 return HTTPFound(self.request.route_path('reset_password'))
456 return HTTPFound(self.request.route_path('reset_password'))
457
457
458 try:
458 try:
459 owner = token.user
459 owner = token.user
460 data = {'email': owner.email, 'token': token.api_key}
460 data = {'email': owner.email, 'token': token.api_key}
461 UserModel().reset_password(data)
461 UserModel().reset_password(data)
462 h.flash(
462 h.flash(
463 _('Your password reset was successful, '
463 _('Your password reset was successful, '
464 'a new password has been sent to your email'),
464 'a new password has been sent to your email'),
465 category='success')
465 category='success')
466 except Exception as e:
466 except Exception as e:
467 log.error(e)
467 log.error(e)
468 return HTTPFound(self.request.route_path('reset_password'))
468 return HTTPFound(self.request.route_path('reset_password'))
469
469
470 return HTTPFound(self.request.route_path('login'))
470 return HTTPFound(self.request.route_path('login'))
@@ -1,258 +1,258 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2016-2020 RhodeCode GmbH
3 # Copyright (C) 2016-2020 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21 import os
21 import os
22 import re
22 import re
23 import sys
23 import sys
24 import logging
24 import logging
25 import signal
25 import signal
26 import tempfile
26 import tempfile
27 from subprocess import Popen, PIPE
27 from subprocess import Popen, PIPE
28 import urllib.parse
28 import urllib.parse
29
29
30 from .base import VcsServer
30 from .base import VcsServer
31
31
32 log = logging.getLogger(__name__)
32 log = logging.getLogger(__name__)
33
33
34
34
35 class SubversionTunnelWrapper(object):
35 class SubversionTunnelWrapper(object):
36 process = None
36 process = None
37
37
38 def __init__(self, server):
38 def __init__(self, server):
39 self.server = server
39 self.server = server
40 self.timeout = 30
40 self.timeout = 30
41 self.stdin = sys.stdin
41 self.stdin = sys.stdin
42 self.stdout = sys.stdout
42 self.stdout = sys.stdout
43 self.svn_conf_fd, self.svn_conf_path = tempfile.mkstemp()
43 self.svn_conf_fd, self.svn_conf_path = tempfile.mkstemp()
44 self.hooks_env_fd, self.hooks_env_path = tempfile.mkstemp()
44 self.hooks_env_fd, self.hooks_env_path = tempfile.mkstemp()
45
45
46 self.read_only = True # flag that we set to make the hooks readonly
46 self.read_only = True # flag that we set to make the hooks readonly
47
47
48 def create_svn_config(self):
48 def create_svn_config(self):
49 content = (
49 content = (
50 '[general]\n'
50 '[general]\n'
51 'hooks-env = {}\n').format(self.hooks_env_path)
51 'hooks-env = {}\n').format(self.hooks_env_path)
52 with os.fdopen(self.svn_conf_fd, 'w') as config_file:
52 with os.fdopen(self.svn_conf_fd, 'w') as config_file:
53 config_file.write(content)
53 config_file.write(content)
54
54
55 def create_hooks_env(self):
55 def create_hooks_env(self):
56 content = (
56 content = (
57 '[default]\n'
57 '[default]\n'
58 'LANG = en_US.UTF-8\n')
58 'LANG = en_US.UTF-8\n')
59 if self.read_only:
59 if self.read_only:
60 content += 'SSH_READ_ONLY = 1\n'
60 content += 'SSH_READ_ONLY = 1\n'
61 with os.fdopen(self.hooks_env_fd, 'w') as hooks_env_file:
61 with os.fdopen(self.hooks_env_fd, 'w') as hooks_env_file:
62 hooks_env_file.write(content)
62 hooks_env_file.write(content)
63
63
64 def remove_configs(self):
64 def remove_configs(self):
65 os.remove(self.svn_conf_path)
65 os.remove(self.svn_conf_path)
66 os.remove(self.hooks_env_path)
66 os.remove(self.hooks_env_path)
67
67
68 def command(self):
68 def command(self):
69 root = self.server.get_root_store()
69 root = self.server.get_root_store()
70 username = self.server.user.username
70 username = self.server.user.username
71
71
72 command = [
72 command = [
73 self.server.svn_path, '-t',
73 self.server.svn_path, '-t',
74 '--config-file', self.svn_conf_path,
74 '--config-file', self.svn_conf_path,
75 '--tunnel-user', username,
75 '--tunnel-user', username,
76 '-r', root]
76 '-r', root]
77 log.debug("Final CMD: %s", ' '.join(command))
77 log.debug("Final CMD: %s", ' '.join(command))
78 return command
78 return command
79
79
80 def start(self):
80 def start(self):
81 command = self.command()
81 command = self.command()
82 self.process = Popen(' '.join(command), stdin=PIPE, shell=True)
82 self.process = Popen(' '.join(command), stdin=PIPE, shell=True)
83
83
84 def sync(self):
84 def sync(self):
85 while self.process.poll() is None:
85 while self.process.poll() is None:
86 next_byte = self.stdin.read(1)
86 next_byte = self.stdin.read(1)
87 if not next_byte:
87 if not next_byte:
88 break
88 break
89 self.process.stdin.write(next_byte)
89 self.process.stdin.write(next_byte)
90 self.remove_configs()
90 self.remove_configs()
91
91
92 @property
92 @property
93 def return_code(self):
93 def return_code(self):
94 return self.process.returncode
94 return self.process.returncode
95
95
96 def get_first_client_response(self):
96 def get_first_client_response(self):
97 signal.signal(signal.SIGALRM, self.interrupt)
97 signal.signal(signal.SIGALRM, self.interrupt)
98 signal.alarm(self.timeout)
98 signal.alarm(self.timeout)
99 first_response = self._read_first_client_response()
99 first_response = self._read_first_client_response()
100 signal.alarm(0)
100 signal.alarm(0)
101 return (self._parse_first_client_response(first_response)
101 return (self._parse_first_client_response(first_response)
102 if first_response else None)
102 if first_response else None)
103
103
104 def patch_first_client_response(self, response, **kwargs):
104 def patch_first_client_response(self, response, **kwargs):
105 self.create_hooks_env()
105 self.create_hooks_env()
106 data = response.copy()
106 data = response.copy()
107 data.update(kwargs)
107 data.update(kwargs)
108 data['url'] = self._svn_string(data['url'])
108 data['url'] = self._svn_string(data['url'])
109 data['ra_client'] = self._svn_string(data['ra_client'])
109 data['ra_client'] = self._svn_string(data['ra_client'])
110 data['client'] = data['client'] or ''
110 data['client'] = data['client'] or ''
111 buffer_ = (
111 buffer_ = (
112 "( {version} ( {capabilities} ) {url}{ra_client}"
112 "( {version} ( {capabilities} ) {url}{ra_client}"
113 "( {client}) ) ".format(**data))
113 "( {client}) ) ".format(**data))
114 self.process.stdin.write(buffer_)
114 self.process.stdin.write(buffer_)
115
115
116 def fail(self, message):
116 def fail(self, message):
117 print("( failure ( ( 210005 {message} 0: 0 ) ) )".format(
117 print("( failure ( ( 210005 {message} 0: 0 ) ) )".format(
118 message=self._svn_string(message)))
118 message=self._svn_string(message)))
119 self.remove_configs()
119 self.remove_configs()
120 self.process.kill()
120 self.process.kill()
121 return 1
121 return 1
122
122
123 def interrupt(self, signum, frame):
123 def interrupt(self, signum, frame):
124 self.fail("Exited by timeout")
124 self.fail("Exited by timeout")
125
125
126 def _svn_string(self, str_):
126 def _svn_string(self, str_):
127 if not str_:
127 if not str_:
128 return ''
128 return ''
129 return '{length}:{string} '.format(length=len(str_), string=str_)
129 return '{length}:{string} '.format(length=len(str_), string=str_)
130
130
131 def _read_first_client_response(self):
131 def _read_first_client_response(self):
132 buffer_ = ""
132 buffer_ = ""
133 brackets_stack = []
133 brackets_stack = []
134 while True:
134 while True:
135 next_byte = self.stdin.read(1)
135 next_byte = self.stdin.read(1)
136 buffer_ += next_byte
136 buffer_ += next_byte
137 if next_byte == "(":
137 if next_byte == "(":
138 brackets_stack.append(next_byte)
138 brackets_stack.append(next_byte)
139 elif next_byte == ")":
139 elif next_byte == ")":
140 brackets_stack.pop()
140 brackets_stack.pop()
141 elif next_byte == " " and not brackets_stack:
141 elif next_byte == " " and not brackets_stack:
142 break
142 break
143
143
144 return buffer_
144 return buffer_
145
145
146 def _parse_first_client_response(self, buffer_):
146 def _parse_first_client_response(self, buffer_):
147 """
147 """
148 According to the Subversion RA protocol, the first request
148 According to the Subversion RA protocol, the first request
149 should look like:
149 should look like:
150
150
151 ( version:number ( cap:word ... ) url:string ? ra-client:string
151 ( version:number ( cap:word ... ) url:string ? ra-client:string
152 ( ? client:string ) )
152 ( ? client:string ) )
153
153
154 Please check https://svn.apache.org/repos/asf/subversion/trunk/subversion/libsvn_ra_svn/protocol
154 Please check https://svn.apache.org/repos/asf/subversion/trunk/subversion/libsvn_ra_svn/protocol
155 """
155 """
156 version_re = r'(?P<version>\d+)'
156 version_re = r'(?P<version>\d+)'
157 capabilities_re = r'\(\s(?P<capabilities>[\w\d\-\ ]+)\s\)'
157 capabilities_re = r'\(\s(?P<capabilities>[\w\d\-\ ]+)\s\)'
158 url_re = r'\d+\:(?P<url>[\W\w]+)'
158 url_re = r'\d+\:(?P<url>[\W\w]+)'
159 ra_client_re = r'(\d+\:(?P<ra_client>[\W\w]+)\s)'
159 ra_client_re = r'(\d+\:(?P<ra_client>[\W\w]+)\s)'
160 client_re = r'(\d+\:(?P<client>[\W\w]+)\s)*'
160 client_re = r'(\d+\:(?P<client>[\W\w]+)\s)*'
161 regex = re.compile(
161 regex = re.compile(
162 r'^\(\s{version}\s{capabilities}\s{url}\s{ra_client}'
162 r'^\(\s{version}\s{capabilities}\s{url}\s{ra_client}'
163 r'\(\s{client}\)\s\)\s*$'.format(
163 r'\(\s{client}\)\s\)\s*$'.format(
164 version=version_re, capabilities=capabilities_re,
164 version=version_re, capabilities=capabilities_re,
165 url=url_re, ra_client=ra_client_re, client=client_re))
165 url=url_re, ra_client=ra_client_re, client=client_re))
166 matcher = regex.match(buffer_)
166 matcher = regex.match(buffer_)
167
167
168 return matcher.groupdict() if matcher else None
168 return matcher.groupdict() if matcher else None
169
169
170 def _match_repo_name(self, url):
170 def _match_repo_name(self, url):
171 """
171 """
172 Given an server url, try to match it against ALL known repository names.
172 Given an server url, try to match it against ALL known repository names.
173 This handles a tricky SVN case for SSH and subdir commits.
173 This handles a tricky SVN case for SSH and subdir commits.
174 E.g if our repo name is my-svn-repo, a svn commit on file in a subdir would
174 E.g if our repo name is my-svn-repo, a svn commit on file in a subdir would
175 result in the url with this subdir added.
175 result in the url with this subdir added.
176 """
176 """
177 # case 1 direct match, we don't do any "heavy" lookups
177 # case 1 direct match, we don't do any "heavy" lookups
178 if url in self.server.user_permissions:
178 if url in self.server.user_permissions:
179 return url
179 return url
180
180
181 log.debug('Extracting repository name from subdir path %s', url)
181 log.debug('Extracting repository name from subdir path %s', url)
182 # case 2 we check all permissions, and match closes possible case...
182 # case 2 we check all permissions, and match closes possible case...
183 # NOTE(dan): In this case we only know that url has a subdir parts, it's safe
183 # NOTE(dan): In this case we only know that url has a subdir parts, it's safe
184 # to assume that it will have the repo name as prefix, we ensure the prefix
184 # to assume that it will have the repo name as prefix, we ensure the prefix
185 # for similar repositories isn't matched by adding a /
185 # for similar repositories isn't matched by adding a /
186 # e.g subgroup/repo-name/ and subgroup/repo-name-1/ would work correct.
186 # e.g subgroup/repo-name/ and subgroup/repo-name-1/ would work correct.
187 for repo_name in self.server.user_permissions:
187 for repo_name in self.server.user_permissions:
188 repo_name_prefix = repo_name + '/'
188 repo_name_prefix = repo_name + '/'
189 if url.startswith(repo_name_prefix):
189 if url.startswith(repo_name_prefix):
190 log.debug('Found prefix %s match, returning proper repository name',
190 log.debug('Found prefix %s match, returning proper repository name',
191 repo_name_prefix)
191 repo_name_prefix)
192 return repo_name
192 return repo_name
193
193
194 return
194 return
195
195
196 def run(self, extras):
196 def run(self, extras):
197 action = 'pull'
197 action = 'pull'
198 self.create_svn_config()
198 self.create_svn_config()
199 self.start()
199 self.start()
200
200
201 first_response = self.get_first_client_response()
201 first_response = self.get_first_client_response()
202 if not first_response:
202 if not first_response:
203 return self.fail("Repository name cannot be extracted")
203 return self.fail("Repository name cannot be extracted")
204
204
205 url_parts = urllib.parse.urlparse.urlparse(first_response['url'])
205 url_parts = urllib.parse.urlparse(first_response['url'])
206
206
207 self.server.repo_name = self._match_repo_name(url_parts.path.strip('/'))
207 self.server.repo_name = self._match_repo_name(url_parts.path.strip('/'))
208
208
209 exit_code = self.server._check_permissions(action)
209 exit_code = self.server._check_permissions(action)
210 if exit_code:
210 if exit_code:
211 return exit_code
211 return exit_code
212
212
213 # set the readonly flag to False if we have proper permissions
213 # set the readonly flag to False if we have proper permissions
214 if self.server.has_write_perm():
214 if self.server.has_write_perm():
215 self.read_only = False
215 self.read_only = False
216 self.server.update_environment(action=action, extras=extras)
216 self.server.update_environment(action=action, extras=extras)
217
217
218 self.patch_first_client_response(first_response)
218 self.patch_first_client_response(first_response)
219 self.sync()
219 self.sync()
220 return self.return_code
220 return self.return_code
221
221
222
222
223 class SubversionServer(VcsServer):
223 class SubversionServer(VcsServer):
224 backend = 'svn'
224 backend = 'svn'
225 repo_user_agent = 'svn'
225 repo_user_agent = 'svn'
226
226
227 def __init__(self, store, ini_path, repo_name,
227 def __init__(self, store, ini_path, repo_name,
228 user, user_permissions, config, env):
228 user, user_permissions, config, env):
229 super(SubversionServer, self)\
229 super(SubversionServer, self)\
230 .__init__(user, user_permissions, config, env)
230 .__init__(user, user_permissions, config, env)
231 self.store = store
231 self.store = store
232 self.ini_path = ini_path
232 self.ini_path = ini_path
233 # NOTE(dan): repo_name at this point is empty,
233 # NOTE(dan): repo_name at this point is empty,
234 # this is set later in .run() based from parsed input stream
234 # this is set later in .run() based from parsed input stream
235 self.repo_name = repo_name
235 self.repo_name = repo_name
236 self._path = self.svn_path = config.get('app:main', 'ssh.executable.svn')
236 self._path = self.svn_path = config.get('app:main', 'ssh.executable.svn')
237
237
238 self.tunnel = SubversionTunnelWrapper(server=self)
238 self.tunnel = SubversionTunnelWrapper(server=self)
239
239
240 def _handle_tunnel(self, extras):
240 def _handle_tunnel(self, extras):
241
241
242 # pre-auth
242 # pre-auth
243 action = 'pull'
243 action = 'pull'
244 # Special case for SVN, we extract repo name at later stage
244 # Special case for SVN, we extract repo name at later stage
245 # exit_code = self._check_permissions(action)
245 # exit_code = self._check_permissions(action)
246 # if exit_code:
246 # if exit_code:
247 # return exit_code, False
247 # return exit_code, False
248
248
249 req = self.env['request']
249 req = self.env['request']
250 server_url = req.host_url + req.script_name
250 server_url = req.host_url + req.script_name
251 extras['server_url'] = server_url
251 extras['server_url'] = server_url
252
252
253 log.debug('Using %s binaries from path %s', self.backend, self._path)
253 log.debug('Using %s binaries from path %s', self.backend, self._path)
254 exit_code = self.tunnel.run(extras)
254 exit_code = self.tunnel.run(extras)
255
255
256 return exit_code, action == "push"
256 return exit_code, action == "push"
257
257
258
258
@@ -1,580 +1,580 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2011-2020 RhodeCode GmbH
3 # Copyright (C) 2011-2020 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21
21
22 """
22 """
23 Renderer for markup languages with ability to parse using rst or markdown
23 Renderer for markup languages with ability to parse using rst or markdown
24 """
24 """
25
25
26 import re
26 import re
27 import os
27 import os
28 import lxml
28 import lxml
29 import logging
29 import logging
30 import urllib.parse
30 import urllib.parse
31 import bleach
31 import bleach
32
32
33 from mako.lookup import TemplateLookup
33 from mako.lookup import TemplateLookup
34 from mako.template import Template as MakoTemplate
34 from mako.template import Template as MakoTemplate
35
35
36 from docutils.core import publish_parts
36 from docutils.core import publish_parts
37 from docutils.parsers.rst import directives
37 from docutils.parsers.rst import directives
38 from docutils import writers
38 from docutils import writers
39 from docutils.writers import html4css1
39 from docutils.writers import html4css1
40 import markdown
40 import markdown
41
41
42 from rhodecode.lib.markdown_ext import GithubFlavoredMarkdownExtension
42 from rhodecode.lib.markdown_ext import GithubFlavoredMarkdownExtension
43 from rhodecode.lib.utils2 import (safe_unicode, md5_safe, MENTIONS_REGEX)
43 from rhodecode.lib.utils2 import (safe_unicode, md5_safe, MENTIONS_REGEX)
44
44
45 log = logging.getLogger(__name__)
45 log = logging.getLogger(__name__)
46
46
47 # default renderer used to generate automated comments
47 # default renderer used to generate automated comments
48 DEFAULT_COMMENTS_RENDERER = 'rst'
48 DEFAULT_COMMENTS_RENDERER = 'rst'
49
49
50 try:
50 try:
51 from lxml.html import fromstring
51 from lxml.html import fromstring
52 from lxml.html import tostring
52 from lxml.html import tostring
53 except ImportError:
53 except ImportError:
54 log.exception('Failed to import lxml')
54 log.exception('Failed to import lxml')
55 fromstring = None
55 fromstring = None
56 tostring = None
56 tostring = None
57
57
58
58
59 class CustomHTMLTranslator(writers.html4css1.HTMLTranslator):
59 class CustomHTMLTranslator(writers.html4css1.HTMLTranslator):
60 """
60 """
61 Custom HTML Translator used for sandboxing potential
61 Custom HTML Translator used for sandboxing potential
62 JS injections in ref links
62 JS injections in ref links
63 """
63 """
64 def visit_literal_block(self, node):
64 def visit_literal_block(self, node):
65 self.body.append(self.starttag(node, 'pre', CLASS='codehilite literal-block'))
65 self.body.append(self.starttag(node, 'pre', CLASS='codehilite literal-block'))
66
66
67 def visit_reference(self, node):
67 def visit_reference(self, node):
68 if 'refuri' in node.attributes:
68 if 'refuri' in node.attributes:
69 refuri = node['refuri']
69 refuri = node['refuri']
70 if ':' in refuri:
70 if ':' in refuri:
71 prefix, link = refuri.lstrip().split(':', 1)
71 prefix, link = refuri.lstrip().split(':', 1)
72 prefix = prefix or ''
72 prefix = prefix or ''
73
73
74 if prefix.lower() == 'javascript':
74 if prefix.lower() == 'javascript':
75 # we don't allow javascript type of refs...
75 # we don't allow javascript type of refs...
76 node['refuri'] = 'javascript:alert("SandBoxedJavascript")'
76 node['refuri'] = 'javascript:alert("SandBoxedJavascript")'
77
77
78 # old style class requires this...
78 # old style class requires this...
79 return html4css1.HTMLTranslator.visit_reference(self, node)
79 return html4css1.HTMLTranslator.visit_reference(self, node)
80
80
81
81
82 class RhodeCodeWriter(writers.html4css1.Writer):
82 class RhodeCodeWriter(writers.html4css1.Writer):
83 def __init__(self):
83 def __init__(self):
84 writers.Writer.__init__(self)
84 writers.Writer.__init__(self)
85 self.translator_class = CustomHTMLTranslator
85 self.translator_class = CustomHTMLTranslator
86
86
87
87
88 def relative_links(html_source, server_paths):
88 def relative_links(html_source, server_paths):
89 if not html_source:
89 if not html_source:
90 return html_source
90 return html_source
91
91
92 if not fromstring and tostring:
92 if not fromstring and tostring:
93 return html_source
93 return html_source
94
94
95 try:
95 try:
96 doc = lxml.html.fromstring(html_source)
96 doc = lxml.html.fromstring(html_source)
97 except Exception:
97 except Exception:
98 return html_source
98 return html_source
99
99
100 for el in doc.cssselect('img, video'):
100 for el in doc.cssselect('img, video'):
101 src = el.attrib.get('src')
101 src = el.attrib.get('src')
102 if src:
102 if src:
103 el.attrib['src'] = relative_path(src, server_paths['raw'])
103 el.attrib['src'] = relative_path(src, server_paths['raw'])
104
104
105 for el in doc.cssselect('a:not(.gfm)'):
105 for el in doc.cssselect('a:not(.gfm)'):
106 src = el.attrib.get('href')
106 src = el.attrib.get('href')
107 if src:
107 if src:
108 raw_mode = el.attrib['href'].endswith('?raw=1')
108 raw_mode = el.attrib['href'].endswith('?raw=1')
109 if raw_mode:
109 if raw_mode:
110 el.attrib['href'] = relative_path(src, server_paths['raw'])
110 el.attrib['href'] = relative_path(src, server_paths['raw'])
111 else:
111 else:
112 el.attrib['href'] = relative_path(src, server_paths['standard'])
112 el.attrib['href'] = relative_path(src, server_paths['standard'])
113
113
114 return lxml.html.tostring(doc)
114 return lxml.html.tostring(doc)
115
115
116
116
117 def relative_path(path, request_path, is_repo_file=None):
117 def relative_path(path, request_path, is_repo_file=None):
118 """
118 """
119 relative link support, path is a rel path, and request_path is current
119 relative link support, path is a rel path, and request_path is current
120 server path (not absolute)
120 server path (not absolute)
121
121
122 e.g.
122 e.g.
123
123
124 path = '../logo.png'
124 path = '../logo.png'
125 request_path= '/repo/files/path/file.md'
125 request_path= '/repo/files/path/file.md'
126 produces: '/repo/files/logo.png'
126 produces: '/repo/files/logo.png'
127 """
127 """
128 # TODO(marcink): unicode/str support ?
128 # TODO(marcink): unicode/str support ?
129 # maybe=> safe_unicode(urllib.quote(safe_str(final_path), '/:'))
129 # maybe=> safe_unicode(urllib.quote(safe_str(final_path), '/:'))
130
130
131 def dummy_check(p):
131 def dummy_check(p):
132 return True # assume default is a valid file path
132 return True # assume default is a valid file path
133
133
134 is_repo_file = is_repo_file or dummy_check
134 is_repo_file = is_repo_file or dummy_check
135 if not path:
135 if not path:
136 return request_path
136 return request_path
137
137
138 path = safe_unicode(path)
138 path = safe_unicode(path)
139 request_path = safe_unicode(request_path)
139 request_path = safe_unicode(request_path)
140
140
141 if path.startswith((u'data:', u'javascript:', u'#', u':')):
141 if path.startswith((u'data:', u'javascript:', u'#', u':')):
142 # skip data, anchor, invalid links
142 # skip data, anchor, invalid links
143 return path
143 return path
144
144
145 is_absolute = bool(urllib.parse.urlparse.urlparse(path).netloc)
145 is_absolute = bool(urllib.parse.urlparse(path).netloc)
146 if is_absolute:
146 if is_absolute:
147 return path
147 return path
148
148
149 if not request_path:
149 if not request_path:
150 return path
150 return path
151
151
152 if path.startswith(u'/'):
152 if path.startswith(u'/'):
153 path = path[1:]
153 path = path[1:]
154
154
155 if path.startswith(u'./'):
155 if path.startswith(u'./'):
156 path = path[2:]
156 path = path[2:]
157
157
158 parts = request_path.split('/')
158 parts = request_path.split('/')
159 # compute how deep we need to traverse the request_path
159 # compute how deep we need to traverse the request_path
160 depth = 0
160 depth = 0
161
161
162 if is_repo_file(request_path):
162 if is_repo_file(request_path):
163 # if request path is a VALID file, we use a relative path with
163 # if request path is a VALID file, we use a relative path with
164 # one level up
164 # one level up
165 depth += 1
165 depth += 1
166
166
167 while path.startswith(u'../'):
167 while path.startswith(u'../'):
168 depth += 1
168 depth += 1
169 path = path[3:]
169 path = path[3:]
170
170
171 if depth > 0:
171 if depth > 0:
172 parts = parts[:-depth]
172 parts = parts[:-depth]
173
173
174 parts.append(path)
174 parts.append(path)
175 final_path = u'/'.join(parts).lstrip(u'/')
175 final_path = u'/'.join(parts).lstrip(u'/')
176
176
177 return u'/' + final_path
177 return u'/' + final_path
178
178
179
179
180 _cached_markdown_renderer = None
180 _cached_markdown_renderer = None
181
181
182
182
183 def get_markdown_renderer(extensions, output_format):
183 def get_markdown_renderer(extensions, output_format):
184 global _cached_markdown_renderer
184 global _cached_markdown_renderer
185
185
186 if _cached_markdown_renderer is None:
186 if _cached_markdown_renderer is None:
187 _cached_markdown_renderer = markdown.Markdown(
187 _cached_markdown_renderer = markdown.Markdown(
188 extensions=extensions,
188 extensions=extensions,
189 enable_attributes=False, output_format=output_format)
189 enable_attributes=False, output_format=output_format)
190 return _cached_markdown_renderer
190 return _cached_markdown_renderer
191
191
192
192
193 _cached_markdown_renderer_flavored = None
193 _cached_markdown_renderer_flavored = None
194
194
195
195
196 def get_markdown_renderer_flavored(extensions, output_format):
196 def get_markdown_renderer_flavored(extensions, output_format):
197 global _cached_markdown_renderer_flavored
197 global _cached_markdown_renderer_flavored
198
198
199 if _cached_markdown_renderer_flavored is None:
199 if _cached_markdown_renderer_flavored is None:
200 _cached_markdown_renderer_flavored = markdown.Markdown(
200 _cached_markdown_renderer_flavored = markdown.Markdown(
201 extensions=extensions + [GithubFlavoredMarkdownExtension()],
201 extensions=extensions + [GithubFlavoredMarkdownExtension()],
202 enable_attributes=False, output_format=output_format)
202 enable_attributes=False, output_format=output_format)
203 return _cached_markdown_renderer_flavored
203 return _cached_markdown_renderer_flavored
204
204
205
205
206 class MarkupRenderer(object):
206 class MarkupRenderer(object):
207 RESTRUCTUREDTEXT_DISALLOWED_DIRECTIVES = ['include', 'meta', 'raw']
207 RESTRUCTUREDTEXT_DISALLOWED_DIRECTIVES = ['include', 'meta', 'raw']
208
208
209 MARKDOWN_PAT = re.compile(r'\.(md|mkdn?|mdown|markdown)$', re.IGNORECASE)
209 MARKDOWN_PAT = re.compile(r'\.(md|mkdn?|mdown|markdown)$', re.IGNORECASE)
210 RST_PAT = re.compile(r'\.re?st$', re.IGNORECASE)
210 RST_PAT = re.compile(r'\.re?st$', re.IGNORECASE)
211 JUPYTER_PAT = re.compile(r'\.(ipynb)$', re.IGNORECASE)
211 JUPYTER_PAT = re.compile(r'\.(ipynb)$', re.IGNORECASE)
212 PLAIN_PAT = re.compile(r'^readme$', re.IGNORECASE)
212 PLAIN_PAT = re.compile(r'^readme$', re.IGNORECASE)
213
213
214 URL_PAT = re.compile(r'(http[s]?://(?:[a-zA-Z]|[0-9]|[$-_@.&+]'
214 URL_PAT = re.compile(r'(http[s]?://(?:[a-zA-Z]|[0-9]|[$-_@.&+]'
215 r'|[!*\(\),]|(?:%[0-9a-fA-F][0-9a-fA-F]))+)')
215 r'|[!*\(\),]|(?:%[0-9a-fA-F][0-9a-fA-F]))+)')
216
216
217 MENTION_PAT = re.compile(MENTIONS_REGEX)
217 MENTION_PAT = re.compile(MENTIONS_REGEX)
218
218
219 extensions = ['markdown.extensions.codehilite', 'markdown.extensions.extra',
219 extensions = ['markdown.extensions.codehilite', 'markdown.extensions.extra',
220 'markdown.extensions.def_list', 'markdown.extensions.sane_lists']
220 'markdown.extensions.def_list', 'markdown.extensions.sane_lists']
221
221
222 output_format = 'html4'
222 output_format = 'html4'
223
223
224 # extension together with weights. Lower is first means we control how
224 # extension together with weights. Lower is first means we control how
225 # extensions are attached to readme names with those.
225 # extensions are attached to readme names with those.
226 PLAIN_EXTS = [
226 PLAIN_EXTS = [
227 # prefer no extension
227 # prefer no extension
228 ('', 0), # special case that renders READMES names without extension
228 ('', 0), # special case that renders READMES names without extension
229 ('.text', 2), ('.TEXT', 2),
229 ('.text', 2), ('.TEXT', 2),
230 ('.txt', 3), ('.TXT', 3)
230 ('.txt', 3), ('.TXT', 3)
231 ]
231 ]
232
232
233 RST_EXTS = [
233 RST_EXTS = [
234 ('.rst', 1), ('.rest', 1),
234 ('.rst', 1), ('.rest', 1),
235 ('.RST', 2), ('.REST', 2)
235 ('.RST', 2), ('.REST', 2)
236 ]
236 ]
237
237
238 MARKDOWN_EXTS = [
238 MARKDOWN_EXTS = [
239 ('.md', 1), ('.MD', 1),
239 ('.md', 1), ('.MD', 1),
240 ('.mkdn', 2), ('.MKDN', 2),
240 ('.mkdn', 2), ('.MKDN', 2),
241 ('.mdown', 3), ('.MDOWN', 3),
241 ('.mdown', 3), ('.MDOWN', 3),
242 ('.markdown', 4), ('.MARKDOWN', 4)
242 ('.markdown', 4), ('.MARKDOWN', 4)
243 ]
243 ]
244
244
245 def _detect_renderer(self, source, filename=None):
245 def _detect_renderer(self, source, filename=None):
246 """
246 """
247 runs detection of what renderer should be used for generating html
247 runs detection of what renderer should be used for generating html
248 from a markup language
248 from a markup language
249
249
250 filename can be also explicitly a renderer name
250 filename can be also explicitly a renderer name
251
251
252 :param source:
252 :param source:
253 :param filename:
253 :param filename:
254 """
254 """
255
255
256 if MarkupRenderer.MARKDOWN_PAT.findall(filename):
256 if MarkupRenderer.MARKDOWN_PAT.findall(filename):
257 detected_renderer = 'markdown'
257 detected_renderer = 'markdown'
258 elif MarkupRenderer.RST_PAT.findall(filename):
258 elif MarkupRenderer.RST_PAT.findall(filename):
259 detected_renderer = 'rst'
259 detected_renderer = 'rst'
260 elif MarkupRenderer.JUPYTER_PAT.findall(filename):
260 elif MarkupRenderer.JUPYTER_PAT.findall(filename):
261 detected_renderer = 'jupyter'
261 detected_renderer = 'jupyter'
262 elif MarkupRenderer.PLAIN_PAT.findall(filename):
262 elif MarkupRenderer.PLAIN_PAT.findall(filename):
263 detected_renderer = 'plain'
263 detected_renderer = 'plain'
264 else:
264 else:
265 detected_renderer = 'plain'
265 detected_renderer = 'plain'
266
266
267 return getattr(MarkupRenderer, detected_renderer)
267 return getattr(MarkupRenderer, detected_renderer)
268
268
269 @classmethod
269 @classmethod
270 def bleach_clean(cls, text):
270 def bleach_clean(cls, text):
271 from .bleach_whitelist import markdown_attrs, markdown_tags
271 from .bleach_whitelist import markdown_attrs, markdown_tags
272 allowed_tags = markdown_tags
272 allowed_tags = markdown_tags
273 allowed_attrs = markdown_attrs
273 allowed_attrs = markdown_attrs
274
274
275 try:
275 try:
276 return bleach.clean(text, tags=allowed_tags, attributes=allowed_attrs)
276 return bleach.clean(text, tags=allowed_tags, attributes=allowed_attrs)
277 except Exception:
277 except Exception:
278 return 'UNPARSEABLE TEXT'
278 return 'UNPARSEABLE TEXT'
279
279
280 @classmethod
280 @classmethod
281 def renderer_from_filename(cls, filename, exclude):
281 def renderer_from_filename(cls, filename, exclude):
282 """
282 """
283 Detect renderer markdown/rst from filename and optionally use exclude
283 Detect renderer markdown/rst from filename and optionally use exclude
284 list to remove some options. This is mostly used in helpers.
284 list to remove some options. This is mostly used in helpers.
285 Returns None when no renderer can be detected.
285 Returns None when no renderer can be detected.
286 """
286 """
287 def _filter(elements):
287 def _filter(elements):
288 if isinstance(exclude, (list, tuple)):
288 if isinstance(exclude, (list, tuple)):
289 return [x for x in elements if x not in exclude]
289 return [x for x in elements if x not in exclude]
290 return elements
290 return elements
291
291
292 if filename.endswith(
292 if filename.endswith(
293 tuple(_filter([x[0] for x in cls.MARKDOWN_EXTS if x[0]]))):
293 tuple(_filter([x[0] for x in cls.MARKDOWN_EXTS if x[0]]))):
294 return 'markdown'
294 return 'markdown'
295 if filename.endswith(tuple(_filter([x[0] for x in cls.RST_EXTS if x[0]]))):
295 if filename.endswith(tuple(_filter([x[0] for x in cls.RST_EXTS if x[0]]))):
296 return 'rst'
296 return 'rst'
297
297
298 return None
298 return None
299
299
300 def render(self, source, filename=None):
300 def render(self, source, filename=None):
301 """
301 """
302 Renders a given filename using detected renderer
302 Renders a given filename using detected renderer
303 it detects renderers based on file extension or mimetype.
303 it detects renderers based on file extension or mimetype.
304 At last it will just do a simple html replacing new lines with <br/>
304 At last it will just do a simple html replacing new lines with <br/>
305
305
306 :param file_name:
306 :param file_name:
307 :param source:
307 :param source:
308 """
308 """
309
309
310 renderer = self._detect_renderer(source, filename)
310 renderer = self._detect_renderer(source, filename)
311 readme_data = renderer(source)
311 readme_data = renderer(source)
312 return readme_data
312 return readme_data
313
313
314 @classmethod
314 @classmethod
315 def _flavored_markdown(cls, text):
315 def _flavored_markdown(cls, text):
316 """
316 """
317 Github style flavored markdown
317 Github style flavored markdown
318
318
319 :param text:
319 :param text:
320 """
320 """
321
321
322 # Extract pre blocks.
322 # Extract pre blocks.
323 extractions = {}
323 extractions = {}
324
324
325 def pre_extraction_callback(matchobj):
325 def pre_extraction_callback(matchobj):
326 digest = md5_safe(matchobj.group(0))
326 digest = md5_safe(matchobj.group(0))
327 extractions[digest] = matchobj.group(0)
327 extractions[digest] = matchobj.group(0)
328 return "{gfm-extraction-%s}" % digest
328 return "{gfm-extraction-%s}" % digest
329 pattern = re.compile(r'<pre>.*?</pre>', re.MULTILINE | re.DOTALL)
329 pattern = re.compile(r'<pre>.*?</pre>', re.MULTILINE | re.DOTALL)
330 text = re.sub(pattern, pre_extraction_callback, text)
330 text = re.sub(pattern, pre_extraction_callback, text)
331
331
332 # Prevent foo_bar_baz from ending up with an italic word in the middle.
332 # Prevent foo_bar_baz from ending up with an italic word in the middle.
333 def italic_callback(matchobj):
333 def italic_callback(matchobj):
334 s = matchobj.group(0)
334 s = matchobj.group(0)
335 if list(s).count('_') >= 2:
335 if list(s).count('_') >= 2:
336 return s.replace('_', r'\_')
336 return s.replace('_', r'\_')
337 return s
337 return s
338 text = re.sub(r'^(?! {4}|\t)\w+_\w+_\w[\w_]*', italic_callback, text)
338 text = re.sub(r'^(?! {4}|\t)\w+_\w+_\w[\w_]*', italic_callback, text)
339
339
340 # Insert pre block extractions.
340 # Insert pre block extractions.
341 def pre_insert_callback(matchobj):
341 def pre_insert_callback(matchobj):
342 return '\n\n' + extractions[matchobj.group(1)]
342 return '\n\n' + extractions[matchobj.group(1)]
343 text = re.sub(r'\{gfm-extraction-([0-9a-f]{32})\}',
343 text = re.sub(r'\{gfm-extraction-([0-9a-f]{32})\}',
344 pre_insert_callback, text)
344 pre_insert_callback, text)
345
345
346 return text
346 return text
347
347
348 @classmethod
348 @classmethod
349 def urlify_text(cls, text):
349 def urlify_text(cls, text):
350 def url_func(match_obj):
350 def url_func(match_obj):
351 url_full = match_obj.groups()[0]
351 url_full = match_obj.groups()[0]
352 return '<a href="%(url)s">%(url)s</a>' % ({'url': url_full})
352 return '<a href="%(url)s">%(url)s</a>' % ({'url': url_full})
353
353
354 return cls.URL_PAT.sub(url_func, text)
354 return cls.URL_PAT.sub(url_func, text)
355
355
356 @classmethod
356 @classmethod
357 def convert_mentions(cls, text, mode):
357 def convert_mentions(cls, text, mode):
358 mention_pat = cls.MENTION_PAT
358 mention_pat = cls.MENTION_PAT
359
359
360 def wrapp(match_obj):
360 def wrapp(match_obj):
361 uname = match_obj.groups()[0]
361 uname = match_obj.groups()[0]
362 hovercard_url = "pyroutes.url('hovercard_username', {'username': '%s'});" % uname
362 hovercard_url = "pyroutes.url('hovercard_username', {'username': '%s'});" % uname
363
363
364 if mode == 'markdown':
364 if mode == 'markdown':
365 tmpl = '<strong class="tooltip-hovercard" data-hovercard-alt="{uname}" data-hovercard-url="{hovercard_url}">@{uname}</strong>'
365 tmpl = '<strong class="tooltip-hovercard" data-hovercard-alt="{uname}" data-hovercard-url="{hovercard_url}">@{uname}</strong>'
366 elif mode == 'rst':
366 elif mode == 'rst':
367 tmpl = ' **@{uname}** '
367 tmpl = ' **@{uname}** '
368 else:
368 else:
369 raise ValueError('mode must be rst or markdown')
369 raise ValueError('mode must be rst or markdown')
370
370
371 return tmpl.format(**{'uname': uname,
371 return tmpl.format(**{'uname': uname,
372 'hovercard_url': hovercard_url})
372 'hovercard_url': hovercard_url})
373
373
374 return mention_pat.sub(wrapp, text).strip()
374 return mention_pat.sub(wrapp, text).strip()
375
375
376 @classmethod
376 @classmethod
377 def plain(cls, source, universal_newline=True, leading_newline=True):
377 def plain(cls, source, universal_newline=True, leading_newline=True):
378 source = safe_unicode(source)
378 source = safe_unicode(source)
379 if universal_newline:
379 if universal_newline:
380 newline = '\n'
380 newline = '\n'
381 source = newline.join(source.splitlines())
381 source = newline.join(source.splitlines())
382
382
383 rendered_source = cls.urlify_text(source)
383 rendered_source = cls.urlify_text(source)
384 source = ''
384 source = ''
385 if leading_newline:
385 if leading_newline:
386 source += '<br />'
386 source += '<br />'
387 source += rendered_source.replace("\n", '<br />')
387 source += rendered_source.replace("\n", '<br />')
388
388
389 rendered = cls.bleach_clean(source)
389 rendered = cls.bleach_clean(source)
390 return rendered
390 return rendered
391
391
392 @classmethod
392 @classmethod
393 def markdown(cls, source, safe=True, flavored=True, mentions=False,
393 def markdown(cls, source, safe=True, flavored=True, mentions=False,
394 clean_html=True):
394 clean_html=True):
395 """
395 """
396 returns markdown rendered code cleaned by the bleach library
396 returns markdown rendered code cleaned by the bleach library
397 """
397 """
398
398
399 if flavored:
399 if flavored:
400 markdown_renderer = get_markdown_renderer_flavored(
400 markdown_renderer = get_markdown_renderer_flavored(
401 cls.extensions, cls.output_format)
401 cls.extensions, cls.output_format)
402 else:
402 else:
403 markdown_renderer = get_markdown_renderer(
403 markdown_renderer = get_markdown_renderer(
404 cls.extensions, cls.output_format)
404 cls.extensions, cls.output_format)
405
405
406 if mentions:
406 if mentions:
407 mention_hl = cls.convert_mentions(source, mode='markdown')
407 mention_hl = cls.convert_mentions(source, mode='markdown')
408 # we extracted mentions render with this using Mentions false
408 # we extracted mentions render with this using Mentions false
409 return cls.markdown(mention_hl, safe=safe, flavored=flavored,
409 return cls.markdown(mention_hl, safe=safe, flavored=flavored,
410 mentions=False)
410 mentions=False)
411
411
412 source = safe_unicode(source)
412 source = safe_unicode(source)
413
413
414 try:
414 try:
415 if flavored:
415 if flavored:
416 source = cls._flavored_markdown(source)
416 source = cls._flavored_markdown(source)
417 rendered = markdown_renderer.convert(source)
417 rendered = markdown_renderer.convert(source)
418 except Exception:
418 except Exception:
419 log.exception('Error when rendering Markdown')
419 log.exception('Error when rendering Markdown')
420 if safe:
420 if safe:
421 log.debug('Fallback to render in plain mode')
421 log.debug('Fallback to render in plain mode')
422 rendered = cls.plain(source)
422 rendered = cls.plain(source)
423 else:
423 else:
424 raise
424 raise
425
425
426 if clean_html:
426 if clean_html:
427 rendered = cls.bleach_clean(rendered)
427 rendered = cls.bleach_clean(rendered)
428 return rendered
428 return rendered
429
429
430 @classmethod
430 @classmethod
431 def rst(cls, source, safe=True, mentions=False, clean_html=False):
431 def rst(cls, source, safe=True, mentions=False, clean_html=False):
432 if mentions:
432 if mentions:
433 mention_hl = cls.convert_mentions(source, mode='rst')
433 mention_hl = cls.convert_mentions(source, mode='rst')
434 # we extracted mentions render with this using Mentions false
434 # we extracted mentions render with this using Mentions false
435 return cls.rst(mention_hl, safe=safe, mentions=False)
435 return cls.rst(mention_hl, safe=safe, mentions=False)
436
436
437 source = safe_unicode(source)
437 source = safe_unicode(source)
438 try:
438 try:
439 docutils_settings = dict(
439 docutils_settings = dict(
440 [(alias, None) for alias in
440 [(alias, None) for alias in
441 cls.RESTRUCTUREDTEXT_DISALLOWED_DIRECTIVES])
441 cls.RESTRUCTUREDTEXT_DISALLOWED_DIRECTIVES])
442
442
443 docutils_settings.update({
443 docutils_settings.update({
444 'input_encoding': 'unicode',
444 'input_encoding': 'unicode',
445 'report_level': 4,
445 'report_level': 4,
446 'syntax_highlight': 'short',
446 'syntax_highlight': 'short',
447 })
447 })
448
448
449 for k, v in docutils_settings.items():
449 for k, v in docutils_settings.items():
450 directives.register_directive(k, v)
450 directives.register_directive(k, v)
451
451
452 parts = publish_parts(source=source,
452 parts = publish_parts(source=source,
453 writer=RhodeCodeWriter(),
453 writer=RhodeCodeWriter(),
454 settings_overrides=docutils_settings)
454 settings_overrides=docutils_settings)
455 rendered = parts["fragment"]
455 rendered = parts["fragment"]
456 if clean_html:
456 if clean_html:
457 rendered = cls.bleach_clean(rendered)
457 rendered = cls.bleach_clean(rendered)
458 return parts['html_title'] + rendered
458 return parts['html_title'] + rendered
459 except Exception:
459 except Exception:
460 log.exception('Error when rendering RST')
460 log.exception('Error when rendering RST')
461 if safe:
461 if safe:
462 log.debug('Fallback to render in plain mode')
462 log.debug('Fallback to render in plain mode')
463 return cls.plain(source)
463 return cls.plain(source)
464 else:
464 else:
465 raise
465 raise
466
466
467 @classmethod
467 @classmethod
468 def jupyter(cls, source, safe=True):
468 def jupyter(cls, source, safe=True):
469 from rhodecode.lib import helpers
469 from rhodecode.lib import helpers
470
470
471 from traitlets.config import Config
471 from traitlets.config import Config
472 import nbformat
472 import nbformat
473 from nbconvert import HTMLExporter
473 from nbconvert import HTMLExporter
474 from nbconvert.preprocessors import Preprocessor
474 from nbconvert.preprocessors import Preprocessor
475
475
476 class CustomHTMLExporter(HTMLExporter):
476 class CustomHTMLExporter(HTMLExporter):
477 def _template_file_default(self):
477 def _template_file_default(self):
478 return 'basic'
478 return 'basic'
479
479
480 class Sandbox(Preprocessor):
480 class Sandbox(Preprocessor):
481
481
482 def preprocess(self, nb, resources):
482 def preprocess(self, nb, resources):
483 sandbox_text = 'SandBoxed(IPython.core.display.Javascript object)'
483 sandbox_text = 'SandBoxed(IPython.core.display.Javascript object)'
484 for cell in nb['cells']:
484 for cell in nb['cells']:
485 if not safe:
485 if not safe:
486 continue
486 continue
487
487
488 if 'outputs' in cell:
488 if 'outputs' in cell:
489 for cell_output in cell['outputs']:
489 for cell_output in cell['outputs']:
490 if 'data' in cell_output:
490 if 'data' in cell_output:
491 if 'application/javascript' in cell_output['data']:
491 if 'application/javascript' in cell_output['data']:
492 cell_output['data']['text/plain'] = sandbox_text
492 cell_output['data']['text/plain'] = sandbox_text
493 cell_output['data'].pop('application/javascript', None)
493 cell_output['data'].pop('application/javascript', None)
494
494
495 if 'source' in cell and cell['cell_type'] == 'markdown':
495 if 'source' in cell and cell['cell_type'] == 'markdown':
496 # sanitize similar like in markdown
496 # sanitize similar like in markdown
497 cell['source'] = cls.bleach_clean(cell['source'])
497 cell['source'] = cls.bleach_clean(cell['source'])
498
498
499 return nb, resources
499 return nb, resources
500
500
501 def _sanitize_resources(input_resources):
501 def _sanitize_resources(input_resources):
502 """
502 """
503 Skip/sanitize some of the CSS generated and included in jupyter
503 Skip/sanitize some of the CSS generated and included in jupyter
504 so it doesn't messes up UI so much
504 so it doesn't messes up UI so much
505 """
505 """
506
506
507 # TODO(marcink): probably we should replace this with whole custom
507 # TODO(marcink): probably we should replace this with whole custom
508 # CSS set that doesn't screw up, but jupyter generated html has some
508 # CSS set that doesn't screw up, but jupyter generated html has some
509 # special markers, so it requires Custom HTML exporter template with
509 # special markers, so it requires Custom HTML exporter template with
510 # _default_template_path_default, to achieve that
510 # _default_template_path_default, to achieve that
511
511
512 # strip the reset CSS
512 # strip the reset CSS
513 input_resources[0] = input_resources[0][input_resources[0].find('/*! Source'):]
513 input_resources[0] = input_resources[0][input_resources[0].find('/*! Source'):]
514 return input_resources
514 return input_resources
515
515
516 def as_html(notebook):
516 def as_html(notebook):
517 conf = Config()
517 conf = Config()
518 conf.CustomHTMLExporter.preprocessors = [Sandbox]
518 conf.CustomHTMLExporter.preprocessors = [Sandbox]
519 html_exporter = CustomHTMLExporter(config=conf)
519 html_exporter = CustomHTMLExporter(config=conf)
520
520
521 (body, resources) = html_exporter.from_notebook_node(notebook)
521 (body, resources) = html_exporter.from_notebook_node(notebook)
522 header = '<!-- ## IPYTHON NOTEBOOK RENDERING ## -->'
522 header = '<!-- ## IPYTHON NOTEBOOK RENDERING ## -->'
523 js = MakoTemplate(r'''
523 js = MakoTemplate(r'''
524 <!-- MathJax configuration -->
524 <!-- MathJax configuration -->
525 <script type="text/x-mathjax-config">
525 <script type="text/x-mathjax-config">
526 MathJax.Hub.Config({
526 MathJax.Hub.Config({
527 jax: ["input/TeX","output/HTML-CSS", "output/PreviewHTML"],
527 jax: ["input/TeX","output/HTML-CSS", "output/PreviewHTML"],
528 extensions: ["tex2jax.js","MathMenu.js","MathZoom.js", "fast-preview.js", "AssistiveMML.js", "[Contrib]/a11y/accessibility-menu.js"],
528 extensions: ["tex2jax.js","MathMenu.js","MathZoom.js", "fast-preview.js", "AssistiveMML.js", "[Contrib]/a11y/accessibility-menu.js"],
529 TeX: {
529 TeX: {
530 extensions: ["AMSmath.js","AMSsymbols.js","noErrors.js","noUndefined.js"]
530 extensions: ["AMSmath.js","AMSsymbols.js","noErrors.js","noUndefined.js"]
531 },
531 },
532 tex2jax: {
532 tex2jax: {
533 inlineMath: [ ['$','$'], ["\\(","\\)"] ],
533 inlineMath: [ ['$','$'], ["\\(","\\)"] ],
534 displayMath: [ ['$$','$$'], ["\\[","\\]"] ],
534 displayMath: [ ['$$','$$'], ["\\[","\\]"] ],
535 processEscapes: true,
535 processEscapes: true,
536 processEnvironments: true
536 processEnvironments: true
537 },
537 },
538 // Center justify equations in code and markdown cells. Elsewhere
538 // Center justify equations in code and markdown cells. Elsewhere
539 // we use CSS to left justify single line equations in code cells.
539 // we use CSS to left justify single line equations in code cells.
540 displayAlign: 'center',
540 displayAlign: 'center',
541 "HTML-CSS": {
541 "HTML-CSS": {
542 styles: {'.MathJax_Display': {"margin": 0}},
542 styles: {'.MathJax_Display': {"margin": 0}},
543 linebreaks: { automatic: true },
543 linebreaks: { automatic: true },
544 availableFonts: ["STIX", "TeX"]
544 availableFonts: ["STIX", "TeX"]
545 },
545 },
546 showMathMenu: false
546 showMathMenu: false
547 });
547 });
548 </script>
548 </script>
549 <!-- End of MathJax configuration -->
549 <!-- End of MathJax configuration -->
550 <script src="${h.asset('js/src/math_jax/MathJax.js')}"></script>
550 <script src="${h.asset('js/src/math_jax/MathJax.js')}"></script>
551 ''').render(h=helpers)
551 ''').render(h=helpers)
552
552
553 css = MakoTemplate(r'''
553 css = MakoTemplate(r'''
554 <link rel="stylesheet" type="text/css" href="${h.asset('css/style-ipython.css', ver=ver)}" media="screen"/>
554 <link rel="stylesheet" type="text/css" href="${h.asset('css/style-ipython.css', ver=ver)}" media="screen"/>
555 ''').render(h=helpers, ver='ver1')
555 ''').render(h=helpers, ver='ver1')
556
556
557 body = '\n'.join([header, css, js, body])
557 body = '\n'.join([header, css, js, body])
558 return body, resources
558 return body, resources
559
559
560 notebook = nbformat.reads(source, as_version=4)
560 notebook = nbformat.reads(source, as_version=4)
561 (body, resources) = as_html(notebook)
561 (body, resources) = as_html(notebook)
562 return body
562 return body
563
563
564
564
565 class RstTemplateRenderer(object):
565 class RstTemplateRenderer(object):
566
566
567 def __init__(self):
567 def __init__(self):
568 base = os.path.abspath(os.path.dirname(os.path.dirname(__file__)))
568 base = os.path.abspath(os.path.dirname(os.path.dirname(__file__)))
569 rst_template_dirs = [os.path.join(base, 'templates', 'rst_templates')]
569 rst_template_dirs = [os.path.join(base, 'templates', 'rst_templates')]
570 self.template_store = TemplateLookup(
570 self.template_store = TemplateLookup(
571 directories=rst_template_dirs,
571 directories=rst_template_dirs,
572 input_encoding='utf-8',
572 input_encoding='utf-8',
573 imports=['from rhodecode.lib import helpers as h'])
573 imports=['from rhodecode.lib import helpers as h'])
574
574
575 def _get_template(self, templatename):
575 def _get_template(self, templatename):
576 return self.template_store.get_template(templatename)
576 return self.template_store.get_template(templatename)
577
577
578 def render(self, template_name, **kwargs):
578 def render(self, template_name, **kwargs):
579 template = self._get_template(template_name)
579 template = self._get_template(template_name)
580 return template.render(**kwargs)
580 return template.render(**kwargs)
@@ -1,229 +1,229 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2010-2020 RhodeCode GmbH
3 # Copyright (C) 2010-2020 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21 import base64
21 import base64
22 import logging
22 import logging
23 import urllib.request, urllib.parse, urllib.error
23 import urllib.request, urllib.parse, urllib.error
24 import urllib.parse
24 import urllib.parse
25
25
26 import requests
26 import requests
27 from pyramid.httpexceptions import HTTPNotAcceptable
27 from pyramid.httpexceptions import HTTPNotAcceptable
28
28
29 from rhodecode.lib import rc_cache
29 from rhodecode.lib import rc_cache
30 from rhodecode.lib.middleware import simplevcs
30 from rhodecode.lib.middleware import simplevcs
31 from rhodecode.lib.utils import is_valid_repo
31 from rhodecode.lib.utils import is_valid_repo
32 from rhodecode.lib.utils2 import str2bool, safe_int, safe_str
32 from rhodecode.lib.utils2 import str2bool, safe_int, safe_str
33 from rhodecode.lib.ext_json import json
33 from rhodecode.lib.ext_json import json
34 from rhodecode.lib.hooks_daemon import store_txn_id_data
34 from rhodecode.lib.hooks_daemon import store_txn_id_data
35
35
36
36
37 log = logging.getLogger(__name__)
37 log = logging.getLogger(__name__)
38
38
39
39
40 class SimpleSvnApp(object):
40 class SimpleSvnApp(object):
41 IGNORED_HEADERS = [
41 IGNORED_HEADERS = [
42 'connection', 'keep-alive', 'content-encoding',
42 'connection', 'keep-alive', 'content-encoding',
43 'transfer-encoding', 'content-length']
43 'transfer-encoding', 'content-length']
44 rc_extras = {}
44 rc_extras = {}
45
45
46 def __init__(self, config):
46 def __init__(self, config):
47 self.config = config
47 self.config = config
48
48
49 def __call__(self, environ, start_response):
49 def __call__(self, environ, start_response):
50 request_headers = self._get_request_headers(environ)
50 request_headers = self._get_request_headers(environ)
51 data = environ['wsgi.input']
51 data = environ['wsgi.input']
52 req_method = environ['REQUEST_METHOD']
52 req_method = environ['REQUEST_METHOD']
53 has_content_length = 'CONTENT_LENGTH' in environ
53 has_content_length = 'CONTENT_LENGTH' in environ
54 path_info = self._get_url(
54 path_info = self._get_url(
55 self.config.get('subversion_http_server_url', ''), environ['PATH_INFO'])
55 self.config.get('subversion_http_server_url', ''), environ['PATH_INFO'])
56 transfer_encoding = environ.get('HTTP_TRANSFER_ENCODING', '')
56 transfer_encoding = environ.get('HTTP_TRANSFER_ENCODING', '')
57 log.debug('Handling: %s method via `%s`', req_method, path_info)
57 log.debug('Handling: %s method via `%s`', req_method, path_info)
58
58
59 # stream control flag, based on request and content type...
59 # stream control flag, based on request and content type...
60 stream = False
60 stream = False
61
61
62 if req_method in ['MKCOL'] or has_content_length:
62 if req_method in ['MKCOL'] or has_content_length:
63 data_processed = False
63 data_processed = False
64 # read chunk to check if we have txn-with-props
64 # read chunk to check if we have txn-with-props
65 initial_data = data.read(1024)
65 initial_data = data.read(1024)
66 if initial_data.startswith('(create-txn-with-props'):
66 if initial_data.startswith('(create-txn-with-props'):
67 data = initial_data + data.read()
67 data = initial_data + data.read()
68 # store on-the-fly our rc_extra using svn revision properties
68 # store on-the-fly our rc_extra using svn revision properties
69 # those can be read later on in hooks executed so we have a way
69 # those can be read later on in hooks executed so we have a way
70 # to pass in the data into svn hooks
70 # to pass in the data into svn hooks
71 rc_data = base64.urlsafe_b64encode(json.dumps(self.rc_extras))
71 rc_data = base64.urlsafe_b64encode(json.dumps(self.rc_extras))
72 rc_data_len = len(rc_data)
72 rc_data_len = len(rc_data)
73 # header defines data length, and serialized data
73 # header defines data length, and serialized data
74 skel = ' rc-scm-extras {} {}'.format(rc_data_len, rc_data)
74 skel = ' rc-scm-extras {} {}'.format(rc_data_len, rc_data)
75 data = data[:-2] + skel + '))'
75 data = data[:-2] + skel + '))'
76 data_processed = True
76 data_processed = True
77
77
78 if not data_processed:
78 if not data_processed:
79 # NOTE(johbo): Avoid that we end up with sending the request in chunked
79 # NOTE(johbo): Avoid that we end up with sending the request in chunked
80 # transfer encoding (mainly on Gunicorn). If we know the content
80 # transfer encoding (mainly on Gunicorn). If we know the content
81 # length, then we should transfer the payload in one request.
81 # length, then we should transfer the payload in one request.
82 data = initial_data + data.read()
82 data = initial_data + data.read()
83
83
84 if req_method in ['GET', 'PUT'] or transfer_encoding == 'chunked':
84 if req_method in ['GET', 'PUT'] or transfer_encoding == 'chunked':
85 # NOTE(marcink): when getting/uploading files we want to STREAM content
85 # NOTE(marcink): when getting/uploading files we want to STREAM content
86 # back to the client/proxy instead of buffering it here...
86 # back to the client/proxy instead of buffering it here...
87 stream = True
87 stream = True
88
88
89 stream = stream
89 stream = stream
90 log.debug('Calling SVN PROXY at `%s`, using method:%s. Stream: %s',
90 log.debug('Calling SVN PROXY at `%s`, using method:%s. Stream: %s',
91 path_info, req_method, stream)
91 path_info, req_method, stream)
92 try:
92 try:
93 response = requests.request(
93 response = requests.request(
94 req_method, path_info,
94 req_method, path_info,
95 data=data, headers=request_headers, stream=stream)
95 data=data, headers=request_headers, stream=stream)
96 except requests.ConnectionError:
96 except requests.ConnectionError:
97 log.exception('ConnectionError occurred for endpoint %s', path_info)
97 log.exception('ConnectionError occurred for endpoint %s', path_info)
98 raise
98 raise
99
99
100 if response.status_code not in [200, 401]:
100 if response.status_code not in [200, 401]:
101 from rhodecode.lib.utils2 import safe_str
101 from rhodecode.lib.utils2 import safe_str
102 text = '\n{}'.format(safe_str(response.text)) if response.text else ''
102 text = '\n{}'.format(safe_str(response.text)) if response.text else ''
103 if response.status_code >= 500:
103 if response.status_code >= 500:
104 log.error('Got SVN response:%s with text:`%s`', response, text)
104 log.error('Got SVN response:%s with text:`%s`', response, text)
105 else:
105 else:
106 log.debug('Got SVN response:%s with text:`%s`', response, text)
106 log.debug('Got SVN response:%s with text:`%s`', response, text)
107 else:
107 else:
108 log.debug('got response code: %s', response.status_code)
108 log.debug('got response code: %s', response.status_code)
109
109
110 response_headers = self._get_response_headers(response.headers)
110 response_headers = self._get_response_headers(response.headers)
111
111
112 if response.headers.get('SVN-Txn-name'):
112 if response.headers.get('SVN-Txn-name'):
113 svn_tx_id = response.headers.get('SVN-Txn-name')
113 svn_tx_id = response.headers.get('SVN-Txn-name')
114 txn_id = rc_cache.utils.compute_key_from_params(
114 txn_id = rc_cache.utils.compute_key_from_params(
115 self.config['repository'], svn_tx_id)
115 self.config['repository'], svn_tx_id)
116 port = safe_int(self.rc_extras['hooks_uri'].split(':')[-1])
116 port = safe_int(self.rc_extras['hooks_uri'].split(':')[-1])
117 store_txn_id_data(txn_id, {'port': port})
117 store_txn_id_data(txn_id, {'port': port})
118
118
119 start_response(
119 start_response(
120 '{} {}'.format(response.status_code, response.reason),
120 '{} {}'.format(response.status_code, response.reason),
121 response_headers)
121 response_headers)
122 return response.iter_content(chunk_size=1024)
122 return response.iter_content(chunk_size=1024)
123
123
124 def _get_url(self, svn_http_server, path):
124 def _get_url(self, svn_http_server, path):
125 svn_http_server_url = (svn_http_server or '').rstrip('/')
125 svn_http_server_url = (svn_http_server or '').rstrip('/')
126 url_path = urllib.parse.urlparse.urljoin(svn_http_server_url + '/', (path or '').lstrip('/'))
126 url_path = urllib.parse.urljoin(svn_http_server_url + '/', (path or '').lstrip('/'))
127 url_path = urllib.parse.quote(url_path, safe="/:=~+!$,;'")
127 url_path = urllib.parse.quote(url_path, safe="/:=~+!$,;'")
128 return url_path
128 return url_path
129
129
130 def _get_request_headers(self, environ):
130 def _get_request_headers(self, environ):
131 headers = {}
131 headers = {}
132
132
133 for key in environ:
133 for key in environ:
134 if not key.startswith('HTTP_'):
134 if not key.startswith('HTTP_'):
135 continue
135 continue
136 new_key = key.split('_')
136 new_key = key.split('_')
137 new_key = [k.capitalize() for k in new_key[1:]]
137 new_key = [k.capitalize() for k in new_key[1:]]
138 new_key = '-'.join(new_key)
138 new_key = '-'.join(new_key)
139 headers[new_key] = environ[key]
139 headers[new_key] = environ[key]
140
140
141 if 'CONTENT_TYPE' in environ:
141 if 'CONTENT_TYPE' in environ:
142 headers['Content-Type'] = environ['CONTENT_TYPE']
142 headers['Content-Type'] = environ['CONTENT_TYPE']
143
143
144 if 'CONTENT_LENGTH' in environ:
144 if 'CONTENT_LENGTH' in environ:
145 headers['Content-Length'] = environ['CONTENT_LENGTH']
145 headers['Content-Length'] = environ['CONTENT_LENGTH']
146
146
147 return headers
147 return headers
148
148
149 def _get_response_headers(self, headers):
149 def _get_response_headers(self, headers):
150 headers = [
150 headers = [
151 (h, headers[h])
151 (h, headers[h])
152 for h in headers
152 for h in headers
153 if h.lower() not in self.IGNORED_HEADERS
153 if h.lower() not in self.IGNORED_HEADERS
154 ]
154 ]
155
155
156 return headers
156 return headers
157
157
158
158
159 class DisabledSimpleSvnApp(object):
159 class DisabledSimpleSvnApp(object):
160 def __init__(self, config):
160 def __init__(self, config):
161 self.config = config
161 self.config = config
162
162
163 def __call__(self, environ, start_response):
163 def __call__(self, environ, start_response):
164 reason = 'Cannot handle SVN call because: SVN HTTP Proxy is not enabled'
164 reason = 'Cannot handle SVN call because: SVN HTTP Proxy is not enabled'
165 log.warning(reason)
165 log.warning(reason)
166 return HTTPNotAcceptable(reason)(environ, start_response)
166 return HTTPNotAcceptable(reason)(environ, start_response)
167
167
168
168
169 class SimpleSvn(simplevcs.SimpleVCS):
169 class SimpleSvn(simplevcs.SimpleVCS):
170
170
171 SCM = 'svn'
171 SCM = 'svn'
172 READ_ONLY_COMMANDS = ('OPTIONS', 'PROPFIND', 'GET', 'REPORT')
172 READ_ONLY_COMMANDS = ('OPTIONS', 'PROPFIND', 'GET', 'REPORT')
173 DEFAULT_HTTP_SERVER = 'http://localhost:8090'
173 DEFAULT_HTTP_SERVER = 'http://localhost:8090'
174
174
175 def _get_repository_name(self, environ):
175 def _get_repository_name(self, environ):
176 """
176 """
177 Gets repository name out of PATH_INFO header
177 Gets repository name out of PATH_INFO header
178
178
179 :param environ: environ where PATH_INFO is stored
179 :param environ: environ where PATH_INFO is stored
180 """
180 """
181 path = environ['PATH_INFO'].split('!')
181 path = environ['PATH_INFO'].split('!')
182 repo_name = path[0].strip('/')
182 repo_name = path[0].strip('/')
183
183
184 # SVN includes the whole path in it's requests, including
184 # SVN includes the whole path in it's requests, including
185 # subdirectories inside the repo. Therefore we have to search for
185 # subdirectories inside the repo. Therefore we have to search for
186 # the repo root directory.
186 # the repo root directory.
187 if not is_valid_repo(
187 if not is_valid_repo(
188 repo_name, self.base_path, explicit_scm=self.SCM):
188 repo_name, self.base_path, explicit_scm=self.SCM):
189 current_path = ''
189 current_path = ''
190 for component in repo_name.split('/'):
190 for component in repo_name.split('/'):
191 current_path += component
191 current_path += component
192 if is_valid_repo(
192 if is_valid_repo(
193 current_path, self.base_path, explicit_scm=self.SCM):
193 current_path, self.base_path, explicit_scm=self.SCM):
194 return current_path
194 return current_path
195 current_path += '/'
195 current_path += '/'
196
196
197 return repo_name
197 return repo_name
198
198
199 def _get_action(self, environ):
199 def _get_action(self, environ):
200 return (
200 return (
201 'pull'
201 'pull'
202 if environ['REQUEST_METHOD'] in self.READ_ONLY_COMMANDS
202 if environ['REQUEST_METHOD'] in self.READ_ONLY_COMMANDS
203 else 'push')
203 else 'push')
204
204
205 def _should_use_callback_daemon(self, extras, environ, action):
205 def _should_use_callback_daemon(self, extras, environ, action):
206 # only MERGE command triggers hooks, so we don't want to start
206 # only MERGE command triggers hooks, so we don't want to start
207 # hooks server too many times. POST however starts the svn transaction
207 # hooks server too many times. POST however starts the svn transaction
208 # so we also need to run the init of callback daemon of POST
208 # so we also need to run the init of callback daemon of POST
209 if environ['REQUEST_METHOD'] in ['MERGE', 'POST']:
209 if environ['REQUEST_METHOD'] in ['MERGE', 'POST']:
210 return True
210 return True
211 return False
211 return False
212
212
213 def _create_wsgi_app(self, repo_path, repo_name, config):
213 def _create_wsgi_app(self, repo_path, repo_name, config):
214 if self._is_svn_enabled():
214 if self._is_svn_enabled():
215 return SimpleSvnApp(config)
215 return SimpleSvnApp(config)
216 # we don't have http proxy enabled return dummy request handler
216 # we don't have http proxy enabled return dummy request handler
217 return DisabledSimpleSvnApp(config)
217 return DisabledSimpleSvnApp(config)
218
218
219 def _is_svn_enabled(self):
219 def _is_svn_enabled(self):
220 conf = self.repo_vcs_config
220 conf = self.repo_vcs_config
221 return str2bool(conf.get('vcs_svn_proxy', 'http_requests_enabled'))
221 return str2bool(conf.get('vcs_svn_proxy', 'http_requests_enabled'))
222
222
223 def _create_config(self, extras, repo_name, scheme='http'):
223 def _create_config(self, extras, repo_name, scheme='http'):
224 conf = self.repo_vcs_config
224 conf = self.repo_vcs_config
225 server_url = conf.get('vcs_svn_proxy', 'http_server_url')
225 server_url = conf.get('vcs_svn_proxy', 'http_server_url')
226 server_url = server_url or self.DEFAULT_HTTP_SERVER
226 server_url = server_url or self.DEFAULT_HTTP_SERVER
227
227
228 extras['subversion_http_server_url'] = server_url
228 extras['subversion_http_server_url'] = server_url
229 return extras
229 return extras
@@ -1,189 +1,189 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2014-2020 RhodeCode GmbH
3 # Copyright (C) 2014-2020 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21 """
21 """
22 Implementation of the scm_app interface using raw HTTP communication.
22 Implementation of the scm_app interface using raw HTTP communication.
23 """
23 """
24
24
25 import base64
25 import base64
26 import logging
26 import logging
27 import urllib.parse
27 import urllib.parse
28 import wsgiref.util
28 import wsgiref.util
29
29
30 import msgpack
30 import msgpack
31 import requests
31 import requests
32 import webob.request
32 import webob.request
33
33
34 import rhodecode
34 import rhodecode
35
35
36
36
37 log = logging.getLogger(__name__)
37 log = logging.getLogger(__name__)
38
38
39
39
40 def create_git_wsgi_app(repo_path, repo_name, config):
40 def create_git_wsgi_app(repo_path, repo_name, config):
41 url = _vcs_streaming_url() + 'git/'
41 url = _vcs_streaming_url() + 'git/'
42 return VcsHttpProxy(url, repo_path, repo_name, config)
42 return VcsHttpProxy(url, repo_path, repo_name, config)
43
43
44
44
45 def create_hg_wsgi_app(repo_path, repo_name, config):
45 def create_hg_wsgi_app(repo_path, repo_name, config):
46 url = _vcs_streaming_url() + 'hg/'
46 url = _vcs_streaming_url() + 'hg/'
47 return VcsHttpProxy(url, repo_path, repo_name, config)
47 return VcsHttpProxy(url, repo_path, repo_name, config)
48
48
49
49
50 def _vcs_streaming_url():
50 def _vcs_streaming_url():
51 template = 'http://{}/stream/'
51 template = 'http://{}/stream/'
52 return template.format(rhodecode.CONFIG['vcs.server'])
52 return template.format(rhodecode.CONFIG['vcs.server'])
53
53
54
54
55 # TODO: johbo: Avoid the global.
55 # TODO: johbo: Avoid the global.
56 session = requests.Session()
56 session = requests.Session()
57 # Requests speedup, avoid reading .netrc and similar
57 # Requests speedup, avoid reading .netrc and similar
58 session.trust_env = False
58 session.trust_env = False
59
59
60 # prevent urllib3 spawning our logs.
60 # prevent urllib3 spawning our logs.
61 logging.getLogger("requests.packages.urllib3.connectionpool").setLevel(
61 logging.getLogger("requests.packages.urllib3.connectionpool").setLevel(
62 logging.WARNING)
62 logging.WARNING)
63
63
64
64
65 class VcsHttpProxy(object):
65 class VcsHttpProxy(object):
66 """
66 """
67 A WSGI application which proxies vcs requests.
67 A WSGI application which proxies vcs requests.
68
68
69 The goal is to shuffle the data around without touching it. The only
69 The goal is to shuffle the data around without touching it. The only
70 exception is the extra data from the config object which we send to the
70 exception is the extra data from the config object which we send to the
71 server as well.
71 server as well.
72 """
72 """
73
73
74 def __init__(self, url, repo_path, repo_name, config):
74 def __init__(self, url, repo_path, repo_name, config):
75 """
75 """
76 :param str url: The URL of the VCSServer to call.
76 :param str url: The URL of the VCSServer to call.
77 """
77 """
78 self._url = url
78 self._url = url
79 self._repo_name = repo_name
79 self._repo_name = repo_name
80 self._repo_path = repo_path
80 self._repo_path = repo_path
81 self._config = config
81 self._config = config
82 self.rc_extras = {}
82 self.rc_extras = {}
83 log.debug(
83 log.debug(
84 "Creating VcsHttpProxy for repo %s, url %s",
84 "Creating VcsHttpProxy for repo %s, url %s",
85 repo_name, url)
85 repo_name, url)
86
86
87 def __call__(self, environ, start_response):
87 def __call__(self, environ, start_response):
88 config = msgpack.packb(self._config)
88 config = msgpack.packb(self._config)
89 request = webob.request.Request(environ)
89 request = webob.request.Request(environ)
90 request_headers = request.headers
90 request_headers = request.headers
91
91
92 request_headers.update({
92 request_headers.update({
93 # TODO: johbo: Remove this, rely on URL path only
93 # TODO: johbo: Remove this, rely on URL path only
94 'X-RC-Repo-Name': self._repo_name,
94 'X-RC-Repo-Name': self._repo_name,
95 'X-RC-Repo-Path': self._repo_path,
95 'X-RC-Repo-Path': self._repo_path,
96 'X-RC-Path-Info': environ['PATH_INFO'],
96 'X-RC-Path-Info': environ['PATH_INFO'],
97
97
98 'X-RC-Repo-Store': self.rc_extras.get('repo_store'),
98 'X-RC-Repo-Store': self.rc_extras.get('repo_store'),
99 'X-RC-Server-Config-File': self.rc_extras.get('config'),
99 'X-RC-Server-Config-File': self.rc_extras.get('config'),
100
100
101 'X-RC-Auth-User': self.rc_extras.get('username'),
101 'X-RC-Auth-User': self.rc_extras.get('username'),
102 'X-RC-Auth-User-Id': str(self.rc_extras.get('user_id')),
102 'X-RC-Auth-User-Id': str(self.rc_extras.get('user_id')),
103 'X-RC-Auth-User-Ip': self.rc_extras.get('ip'),
103 'X-RC-Auth-User-Ip': self.rc_extras.get('ip'),
104
104
105 # TODO: johbo: Avoid encoding and put this into payload?
105 # TODO: johbo: Avoid encoding and put this into payload?
106 'X-RC-Repo-Config': base64.b64encode(config),
106 'X-RC-Repo-Config': base64.b64encode(config),
107 'X-RC-Locked-Status-Code': rhodecode.CONFIG.get('lock_ret_code'),
107 'X-RC-Locked-Status-Code': rhodecode.CONFIG.get('lock_ret_code'),
108 })
108 })
109
109
110 method = environ['REQUEST_METHOD']
110 method = environ['REQUEST_METHOD']
111
111
112 # Preserve the query string
112 # Preserve the query string
113 url = self._url
113 url = self._url
114 url = urllib.parse.urlparse.urljoin(url, self._repo_name)
114 url = urllib.parse.urljoin(url, self._repo_name)
115 if environ.get('QUERY_STRING'):
115 if environ.get('QUERY_STRING'):
116 url += '?' + environ['QUERY_STRING']
116 url += '?' + environ['QUERY_STRING']
117
117
118 log.debug('http-app: preparing request to: %s', url)
118 log.debug('http-app: preparing request to: %s', url)
119 response = session.request(
119 response = session.request(
120 method,
120 method,
121 url,
121 url,
122 data=_maybe_stream_request(environ),
122 data=_maybe_stream_request(environ),
123 headers=request_headers,
123 headers=request_headers,
124 stream=True)
124 stream=True)
125
125
126 log.debug('http-app: got vcsserver response: %s', response)
126 log.debug('http-app: got vcsserver response: %s', response)
127 if response.status_code >= 500:
127 if response.status_code >= 500:
128 log.error('Exception returned by vcsserver at: %s %s, %s',
128 log.error('Exception returned by vcsserver at: %s %s, %s',
129 url, response.status_code, response.content)
129 url, response.status_code, response.content)
130
130
131 # Preserve the headers of the response, except hop_by_hop ones
131 # Preserve the headers of the response, except hop_by_hop ones
132 response_headers = [
132 response_headers = [
133 (h, v) for h, v in response.headers.items()
133 (h, v) for h, v in response.headers.items()
134 if not wsgiref.util.is_hop_by_hop(h)
134 if not wsgiref.util.is_hop_by_hop(h)
135 ]
135 ]
136
136
137 # Build status argument for start_response callable.
137 # Build status argument for start_response callable.
138 status = '{status_code} {reason_phrase}'.format(
138 status = '{status_code} {reason_phrase}'.format(
139 status_code=response.status_code,
139 status_code=response.status_code,
140 reason_phrase=response.reason)
140 reason_phrase=response.reason)
141
141
142 start_response(status, response_headers)
142 start_response(status, response_headers)
143 return _maybe_stream_response(response)
143 return _maybe_stream_response(response)
144
144
145
145
146 def read_in_chunks(stream_obj, block_size=1024, chunks=-1):
146 def read_in_chunks(stream_obj, block_size=1024, chunks=-1):
147 """
147 """
148 Read Stream in chunks, default chunk size: 1k.
148 Read Stream in chunks, default chunk size: 1k.
149 """
149 """
150 while chunks:
150 while chunks:
151 data = stream_obj.read(block_size)
151 data = stream_obj.read(block_size)
152 if not data:
152 if not data:
153 break
153 break
154 yield data
154 yield data
155 chunks -= 1
155 chunks -= 1
156
156
157
157
158 def _is_request_chunked(environ):
158 def _is_request_chunked(environ):
159 stream = environ.get('HTTP_TRANSFER_ENCODING', '') == 'chunked'
159 stream = environ.get('HTTP_TRANSFER_ENCODING', '') == 'chunked'
160 return stream
160 return stream
161
161
162
162
163 def _maybe_stream_request(environ):
163 def _maybe_stream_request(environ):
164 path = environ['PATH_INFO']
164 path = environ['PATH_INFO']
165 stream = _is_request_chunked(environ)
165 stream = _is_request_chunked(environ)
166 log.debug('handling request `%s` with stream support: %s', path, stream)
166 log.debug('handling request `%s` with stream support: %s', path, stream)
167
167
168 if stream:
168 if stream:
169 # set stream by 256k
169 # set stream by 256k
170 return read_in_chunks(environ['wsgi.input'], block_size=1024 * 256)
170 return read_in_chunks(environ['wsgi.input'], block_size=1024 * 256)
171 else:
171 else:
172 return environ['wsgi.input'].read()
172 return environ['wsgi.input'].read()
173
173
174
174
175 def _maybe_stream_response(response):
175 def _maybe_stream_response(response):
176 """
176 """
177 Try to generate chunks from the response if it is chunked.
177 Try to generate chunks from the response if it is chunked.
178 """
178 """
179 stream = _is_chunked(response)
179 stream = _is_chunked(response)
180 log.debug('returning response with stream: %s', stream)
180 log.debug('returning response with stream: %s', stream)
181 if stream:
181 if stream:
182 # read in 256k Chunks
182 # read in 256k Chunks
183 return response.raw.read_chunked(amt=1024 * 256)
183 return response.raw.read_chunked(amt=1024 * 256)
184 else:
184 else:
185 return [response.content]
185 return [response.content]
186
186
187
187
188 def _is_chunked(response):
188 def _is_chunked(response):
189 return response.headers.get('Transfer-Encoding', '') == 'chunked'
189 return response.headers.get('Transfer-Encoding', '') == 'chunked'
@@ -1,412 +1,412 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2016-2020 RhodeCode GmbH
3 # Copyright (C) 2016-2020 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21 """
21 """
22 Client for the VCSServer implemented based on HTTP.
22 Client for the VCSServer implemented based on HTTP.
23 """
23 """
24
24
25 import copy
25 import copy
26 import logging
26 import logging
27 import threading
27 import threading
28 import time
28 import time
29 import urllib.request, urllib.error, urllib.parse
29 import urllib.request, urllib.error, urllib.parse
30 import urllib.parse
30 import urllib.parse
31 import uuid
31 import uuid
32 import traceback
32 import traceback
33
33
34 import pycurl
34 import pycurl
35 import msgpack
35 import msgpack
36 import requests
36 import requests
37 from requests.packages.urllib3.util.retry import Retry
37 from requests.packages.urllib3.util.retry import Retry
38
38
39 import rhodecode
39 import rhodecode
40 from rhodecode.lib import rc_cache
40 from rhodecode.lib import rc_cache
41 from rhodecode.lib.rc_cache.utils import compute_key_from_params
41 from rhodecode.lib.rc_cache.utils import compute_key_from_params
42 from rhodecode.lib.system_info import get_cert_path
42 from rhodecode.lib.system_info import get_cert_path
43 from rhodecode.lib.vcs import exceptions, CurlSession
43 from rhodecode.lib.vcs import exceptions, CurlSession
44 from rhodecode.lib.utils2 import str2bool
44 from rhodecode.lib.utils2 import str2bool
45
45
46 log = logging.getLogger(__name__)
46 log = logging.getLogger(__name__)
47
47
48
48
49 # TODO: mikhail: Keep it in sync with vcsserver's
49 # TODO: mikhail: Keep it in sync with vcsserver's
50 # HTTPApplication.ALLOWED_EXCEPTIONS
50 # HTTPApplication.ALLOWED_EXCEPTIONS
51 EXCEPTIONS_MAP = {
51 EXCEPTIONS_MAP = {
52 'KeyError': KeyError,
52 'KeyError': KeyError,
53 'URLError': urllib.error.URLError,
53 'URLError': urllib.error.URLError,
54 }
54 }
55
55
56
56
57 def _remote_call(url, payload, exceptions_map, session):
57 def _remote_call(url, payload, exceptions_map, session):
58 try:
58 try:
59 headers = {
59 headers = {
60 'X-RC-Method': payload.get('method'),
60 'X-RC-Method': payload.get('method'),
61 'X-RC-Repo-Name': payload.get('_repo_name')
61 'X-RC-Repo-Name': payload.get('_repo_name')
62 }
62 }
63 response = session.post(url, data=msgpack.packb(payload), headers=headers)
63 response = session.post(url, data=msgpack.packb(payload), headers=headers)
64 except pycurl.error as e:
64 except pycurl.error as e:
65 msg = '{}. \npycurl traceback: {}'.format(e, traceback.format_exc())
65 msg = '{}. \npycurl traceback: {}'.format(e, traceback.format_exc())
66 raise exceptions.HttpVCSCommunicationError(msg)
66 raise exceptions.HttpVCSCommunicationError(msg)
67 except Exception as e:
67 except Exception as e:
68 message = getattr(e, 'message', '')
68 message = getattr(e, 'message', '')
69 if 'Failed to connect' in message:
69 if 'Failed to connect' in message:
70 # gevent doesn't return proper pycurl errors
70 # gevent doesn't return proper pycurl errors
71 raise exceptions.HttpVCSCommunicationError(e)
71 raise exceptions.HttpVCSCommunicationError(e)
72 else:
72 else:
73 raise
73 raise
74
74
75 if response.status_code >= 400:
75 if response.status_code >= 400:
76 log.error('Call to %s returned non 200 HTTP code: %s',
76 log.error('Call to %s returned non 200 HTTP code: %s',
77 url, response.status_code)
77 url, response.status_code)
78 raise exceptions.HttpVCSCommunicationError(repr(response.content))
78 raise exceptions.HttpVCSCommunicationError(repr(response.content))
79
79
80 try:
80 try:
81 response = msgpack.unpackb(response.content)
81 response = msgpack.unpackb(response.content)
82 except Exception:
82 except Exception:
83 log.exception('Failed to decode response %r', response.content)
83 log.exception('Failed to decode response %r', response.content)
84 raise
84 raise
85
85
86 error = response.get('error')
86 error = response.get('error')
87 if error:
87 if error:
88 type_ = error.get('type', 'Exception')
88 type_ = error.get('type', 'Exception')
89 exc = exceptions_map.get(type_, Exception)
89 exc = exceptions_map.get(type_, Exception)
90 exc = exc(error.get('message'))
90 exc = exc(error.get('message'))
91 try:
91 try:
92 exc._vcs_kind = error['_vcs_kind']
92 exc._vcs_kind = error['_vcs_kind']
93 except KeyError:
93 except KeyError:
94 pass
94 pass
95
95
96 try:
96 try:
97 exc._vcs_server_traceback = error['traceback']
97 exc._vcs_server_traceback = error['traceback']
98 exc._vcs_server_org_exc_name = error['org_exc']
98 exc._vcs_server_org_exc_name = error['org_exc']
99 exc._vcs_server_org_exc_tb = error['org_exc_tb']
99 exc._vcs_server_org_exc_tb = error['org_exc_tb']
100 except KeyError:
100 except KeyError:
101 pass
101 pass
102
102
103 raise exc
103 raise exc
104 return response.get('result')
104 return response.get('result')
105
105
106
106
107 def _streaming_remote_call(url, payload, exceptions_map, session, chunk_size):
107 def _streaming_remote_call(url, payload, exceptions_map, session, chunk_size):
108 try:
108 try:
109 headers = {
109 headers = {
110 'X-RC-Method': payload.get('method'),
110 'X-RC-Method': payload.get('method'),
111 'X-RC-Repo-Name': payload.get('_repo_name')
111 'X-RC-Repo-Name': payload.get('_repo_name')
112 }
112 }
113 response = session.post(url, data=msgpack.packb(payload), headers=headers)
113 response = session.post(url, data=msgpack.packb(payload), headers=headers)
114 except pycurl.error as e:
114 except pycurl.error as e:
115 msg = '{}. \npycurl traceback: {}'.format(e, traceback.format_exc())
115 msg = '{}. \npycurl traceback: {}'.format(e, traceback.format_exc())
116 raise exceptions.HttpVCSCommunicationError(msg)
116 raise exceptions.HttpVCSCommunicationError(msg)
117 except Exception as e:
117 except Exception as e:
118 message = getattr(e, 'message', '')
118 message = getattr(e, 'message', '')
119 if 'Failed to connect' in message:
119 if 'Failed to connect' in message:
120 # gevent doesn't return proper pycurl errors
120 # gevent doesn't return proper pycurl errors
121 raise exceptions.HttpVCSCommunicationError(e)
121 raise exceptions.HttpVCSCommunicationError(e)
122 else:
122 else:
123 raise
123 raise
124
124
125 if response.status_code >= 400:
125 if response.status_code >= 400:
126 log.error('Call to %s returned non 200 HTTP code: %s',
126 log.error('Call to %s returned non 200 HTTP code: %s',
127 url, response.status_code)
127 url, response.status_code)
128 raise exceptions.HttpVCSCommunicationError(repr(response.content))
128 raise exceptions.HttpVCSCommunicationError(repr(response.content))
129
129
130 return response.iter_content(chunk_size=chunk_size)
130 return response.iter_content(chunk_size=chunk_size)
131
131
132
132
133 class ServiceConnection(object):
133 class ServiceConnection(object):
134 def __init__(self, server_and_port, backend_endpoint, session_factory):
134 def __init__(self, server_and_port, backend_endpoint, session_factory):
135 self.url = urllib.parse.urlparse.urljoin('http://%s' % server_and_port, backend_endpoint)
135 self.url = urllib.parse.urljoin('http://%s' % server_and_port, backend_endpoint)
136 self._session_factory = session_factory
136 self._session_factory = session_factory
137
137
138 def __getattr__(self, name):
138 def __getattr__(self, name):
139 def f(*args, **kwargs):
139 def f(*args, **kwargs):
140 return self._call(name, *args, **kwargs)
140 return self._call(name, *args, **kwargs)
141 return f
141 return f
142
142
143 @exceptions.map_vcs_exceptions
143 @exceptions.map_vcs_exceptions
144 def _call(self, name, *args, **kwargs):
144 def _call(self, name, *args, **kwargs):
145 payload = {
145 payload = {
146 'id': str(uuid.uuid4()),
146 'id': str(uuid.uuid4()),
147 'method': name,
147 'method': name,
148 'params': {'args': args, 'kwargs': kwargs}
148 'params': {'args': args, 'kwargs': kwargs}
149 }
149 }
150 return _remote_call(
150 return _remote_call(
151 self.url, payload, EXCEPTIONS_MAP, self._session_factory())
151 self.url, payload, EXCEPTIONS_MAP, self._session_factory())
152
152
153
153
154 class RemoteVCSMaker(object):
154 class RemoteVCSMaker(object):
155
155
156 def __init__(self, server_and_port, backend_endpoint, backend_type, session_factory):
156 def __init__(self, server_and_port, backend_endpoint, backend_type, session_factory):
157 self.url = urllib.parse.urlparse.urljoin('http://%s' % server_and_port, backend_endpoint)
157 self.url = urllib.parse.urljoin('http://%s' % server_and_port, backend_endpoint)
158 self.stream_url = urllib.parse.urlparse.urljoin('http://%s' % server_and_port, backend_endpoint+'/stream')
158 self.stream_url = urllib.parse.urljoin('http://%s' % server_and_port, backend_endpoint+'/stream')
159
159
160 self._session_factory = session_factory
160 self._session_factory = session_factory
161 self.backend_type = backend_type
161 self.backend_type = backend_type
162
162
163 @classmethod
163 @classmethod
164 def init_cache_region(cls, repo_id):
164 def init_cache_region(cls, repo_id):
165 cache_namespace_uid = 'cache_repo.{}'.format(repo_id)
165 cache_namespace_uid = 'cache_repo.{}'.format(repo_id)
166 region = rc_cache.get_or_create_region('cache_repo', cache_namespace_uid)
166 region = rc_cache.get_or_create_region('cache_repo', cache_namespace_uid)
167 return region, cache_namespace_uid
167 return region, cache_namespace_uid
168
168
169 def __call__(self, path, repo_id, config, with_wire=None):
169 def __call__(self, path, repo_id, config, with_wire=None):
170 log.debug('%s RepoMaker call on %s', self.backend_type.upper(), path)
170 log.debug('%s RepoMaker call on %s', self.backend_type.upper(), path)
171 return RemoteRepo(path, repo_id, config, self, with_wire=with_wire)
171 return RemoteRepo(path, repo_id, config, self, with_wire=with_wire)
172
172
173 def __getattr__(self, name):
173 def __getattr__(self, name):
174 def remote_attr(*args, **kwargs):
174 def remote_attr(*args, **kwargs):
175 return self._call(name, *args, **kwargs)
175 return self._call(name, *args, **kwargs)
176 return remote_attr
176 return remote_attr
177
177
178 @exceptions.map_vcs_exceptions
178 @exceptions.map_vcs_exceptions
179 def _call(self, func_name, *args, **kwargs):
179 def _call(self, func_name, *args, **kwargs):
180 payload = {
180 payload = {
181 'id': str(uuid.uuid4()),
181 'id': str(uuid.uuid4()),
182 'method': func_name,
182 'method': func_name,
183 'backend': self.backend_type,
183 'backend': self.backend_type,
184 'params': {'args': args, 'kwargs': kwargs}
184 'params': {'args': args, 'kwargs': kwargs}
185 }
185 }
186 url = self.url
186 url = self.url
187 return _remote_call(url, payload, EXCEPTIONS_MAP, self._session_factory())
187 return _remote_call(url, payload, EXCEPTIONS_MAP, self._session_factory())
188
188
189
189
190 class RemoteRepo(object):
190 class RemoteRepo(object):
191 CHUNK_SIZE = 16384
191 CHUNK_SIZE = 16384
192
192
193 def __init__(self, path, repo_id, config, remote_maker, with_wire=None):
193 def __init__(self, path, repo_id, config, remote_maker, with_wire=None):
194 self.url = remote_maker.url
194 self.url = remote_maker.url
195 self.stream_url = remote_maker.stream_url
195 self.stream_url = remote_maker.stream_url
196 self._session = remote_maker._session_factory()
196 self._session = remote_maker._session_factory()
197
197
198 cache_repo_id = self._repo_id_sanitizer(repo_id)
198 cache_repo_id = self._repo_id_sanitizer(repo_id)
199 _repo_name = self._get_repo_name(config, path)
199 _repo_name = self._get_repo_name(config, path)
200 self._cache_region, self._cache_namespace = \
200 self._cache_region, self._cache_namespace = \
201 remote_maker.init_cache_region(cache_repo_id)
201 remote_maker.init_cache_region(cache_repo_id)
202
202
203 with_wire = with_wire or {}
203 with_wire = with_wire or {}
204
204
205 repo_state_uid = with_wire.get('repo_state_uid') or 'state'
205 repo_state_uid = with_wire.get('repo_state_uid') or 'state'
206
206
207 self._wire = {
207 self._wire = {
208 "_repo_name": _repo_name,
208 "_repo_name": _repo_name,
209 "path": path, # repo path
209 "path": path, # repo path
210 "repo_id": repo_id,
210 "repo_id": repo_id,
211 "cache_repo_id": cache_repo_id,
211 "cache_repo_id": cache_repo_id,
212 "config": config,
212 "config": config,
213 "repo_state_uid": repo_state_uid,
213 "repo_state_uid": repo_state_uid,
214 "context": self._create_vcs_cache_context(path, repo_state_uid)
214 "context": self._create_vcs_cache_context(path, repo_state_uid)
215 }
215 }
216
216
217 if with_wire:
217 if with_wire:
218 self._wire.update(with_wire)
218 self._wire.update(with_wire)
219
219
220 # NOTE(johbo): Trading complexity for performance. Avoiding the call to
220 # NOTE(johbo): Trading complexity for performance. Avoiding the call to
221 # log.debug brings a few percent gain even if is is not active.
221 # log.debug brings a few percent gain even if is is not active.
222 if log.isEnabledFor(logging.DEBUG):
222 if log.isEnabledFor(logging.DEBUG):
223 self._call_with_logging = True
223 self._call_with_logging = True
224
224
225 self.cert_dir = get_cert_path(rhodecode.CONFIG.get('__file__'))
225 self.cert_dir = get_cert_path(rhodecode.CONFIG.get('__file__'))
226
226
227 def _get_repo_name(self, config, path):
227 def _get_repo_name(self, config, path):
228 repo_store = config.get('paths', '/')
228 repo_store = config.get('paths', '/')
229 return path.split(repo_store)[-1].lstrip('/')
229 return path.split(repo_store)[-1].lstrip('/')
230
230
231 def _repo_id_sanitizer(self, repo_id):
231 def _repo_id_sanitizer(self, repo_id):
232 pathless = repo_id.replace('/', '__').replace('-', '_')
232 pathless = repo_id.replace('/', '__').replace('-', '_')
233 return ''.join(char if ord(char) < 128 else '_{}_'.format(ord(char)) for char in pathless)
233 return ''.join(char if ord(char) < 128 else '_{}_'.format(ord(char)) for char in pathless)
234
234
235 def __getattr__(self, name):
235 def __getattr__(self, name):
236
236
237 if name.startswith('stream:'):
237 if name.startswith('stream:'):
238 def repo_remote_attr(*args, **kwargs):
238 def repo_remote_attr(*args, **kwargs):
239 return self._call_stream(name, *args, **kwargs)
239 return self._call_stream(name, *args, **kwargs)
240 else:
240 else:
241 def repo_remote_attr(*args, **kwargs):
241 def repo_remote_attr(*args, **kwargs):
242 return self._call(name, *args, **kwargs)
242 return self._call(name, *args, **kwargs)
243
243
244 return repo_remote_attr
244 return repo_remote_attr
245
245
246 def _base_call(self, name, *args, **kwargs):
246 def _base_call(self, name, *args, **kwargs):
247 # TODO: oliver: This is currently necessary pre-call since the
247 # TODO: oliver: This is currently necessary pre-call since the
248 # config object is being changed for hooking scenarios
248 # config object is being changed for hooking scenarios
249 wire = copy.deepcopy(self._wire)
249 wire = copy.deepcopy(self._wire)
250 wire["config"] = wire["config"].serialize()
250 wire["config"] = wire["config"].serialize()
251 wire["config"].append(('vcs', 'ssl_dir', self.cert_dir))
251 wire["config"].append(('vcs', 'ssl_dir', self.cert_dir))
252
252
253 payload = {
253 payload = {
254 'id': str(uuid.uuid4()),
254 'id': str(uuid.uuid4()),
255 'method': name,
255 'method': name,
256 "_repo_name": wire['_repo_name'],
256 "_repo_name": wire['_repo_name'],
257 'params': {'wire': wire, 'args': args, 'kwargs': kwargs}
257 'params': {'wire': wire, 'args': args, 'kwargs': kwargs}
258 }
258 }
259
259
260 context_uid = wire.get('context')
260 context_uid = wire.get('context')
261 return context_uid, payload
261 return context_uid, payload
262
262
263 def get_local_cache(self, name, args):
263 def get_local_cache(self, name, args):
264 cache_on = False
264 cache_on = False
265 cache_key = ''
265 cache_key = ''
266 local_cache_on = str2bool(rhodecode.CONFIG.get('vcs.methods.cache'))
266 local_cache_on = str2bool(rhodecode.CONFIG.get('vcs.methods.cache'))
267
267
268 cache_methods = [
268 cache_methods = [
269 'branches', 'tags', 'bookmarks',
269 'branches', 'tags', 'bookmarks',
270 'is_large_file', 'is_binary',
270 'is_large_file', 'is_binary',
271 'fctx_size', 'stream:fctx_node_data', 'blob_raw_length',
271 'fctx_size', 'stream:fctx_node_data', 'blob_raw_length',
272 'node_history',
272 'node_history',
273 'revision', 'tree_items',
273 'revision', 'tree_items',
274 'ctx_list', 'ctx_branch', 'ctx_description',
274 'ctx_list', 'ctx_branch', 'ctx_description',
275 'bulk_request',
275 'bulk_request',
276 'assert_correct_path'
276 'assert_correct_path'
277 ]
277 ]
278
278
279 if local_cache_on and name in cache_methods:
279 if local_cache_on and name in cache_methods:
280 cache_on = True
280 cache_on = True
281 repo_state_uid = self._wire['repo_state_uid']
281 repo_state_uid = self._wire['repo_state_uid']
282 call_args = [a for a in args]
282 call_args = [a for a in args]
283 cache_key = compute_key_from_params(repo_state_uid, name, *call_args)
283 cache_key = compute_key_from_params(repo_state_uid, name, *call_args)
284
284
285 return cache_on, cache_key
285 return cache_on, cache_key
286
286
287 @exceptions.map_vcs_exceptions
287 @exceptions.map_vcs_exceptions
288 def _call(self, name, *args, **kwargs):
288 def _call(self, name, *args, **kwargs):
289 context_uid, payload = self._base_call(name, *args, **kwargs)
289 context_uid, payload = self._base_call(name, *args, **kwargs)
290 url = self.url
290 url = self.url
291
291
292 start = time.time()
292 start = time.time()
293 cache_on, cache_key = self.get_local_cache(name, args)
293 cache_on, cache_key = self.get_local_cache(name, args)
294
294
295 @self._cache_region.conditional_cache_on_arguments(
295 @self._cache_region.conditional_cache_on_arguments(
296 namespace=self._cache_namespace, condition=cache_on and cache_key)
296 namespace=self._cache_namespace, condition=cache_on and cache_key)
297 def remote_call(_cache_key):
297 def remote_call(_cache_key):
298 if self._call_with_logging:
298 if self._call_with_logging:
299 log.debug('Calling %s@%s with args:%.10240r. wire_context: %s cache_on: %s',
299 log.debug('Calling %s@%s with args:%.10240r. wire_context: %s cache_on: %s',
300 url, name, args, context_uid, cache_on)
300 url, name, args, context_uid, cache_on)
301 return _remote_call(url, payload, EXCEPTIONS_MAP, self._session)
301 return _remote_call(url, payload, EXCEPTIONS_MAP, self._session)
302
302
303 result = remote_call(cache_key)
303 result = remote_call(cache_key)
304 if self._call_with_logging:
304 if self._call_with_logging:
305 log.debug('Call %s@%s took: %.4fs. wire_context: %s',
305 log.debug('Call %s@%s took: %.4fs. wire_context: %s',
306 url, name, time.time()-start, context_uid)
306 url, name, time.time()-start, context_uid)
307 return result
307 return result
308
308
309 @exceptions.map_vcs_exceptions
309 @exceptions.map_vcs_exceptions
310 def _call_stream(self, name, *args, **kwargs):
310 def _call_stream(self, name, *args, **kwargs):
311 context_uid, payload = self._base_call(name, *args, **kwargs)
311 context_uid, payload = self._base_call(name, *args, **kwargs)
312 payload['chunk_size'] = self.CHUNK_SIZE
312 payload['chunk_size'] = self.CHUNK_SIZE
313 url = self.stream_url
313 url = self.stream_url
314
314
315 start = time.time()
315 start = time.time()
316 cache_on, cache_key = self.get_local_cache(name, args)
316 cache_on, cache_key = self.get_local_cache(name, args)
317
317
318 # Cache is a problem because this is a stream
318 # Cache is a problem because this is a stream
319 def streaming_remote_call(_cache_key):
319 def streaming_remote_call(_cache_key):
320 if self._call_with_logging:
320 if self._call_with_logging:
321 log.debug('Calling %s@%s with args:%.10240r. wire_context: %s cache_on: %s',
321 log.debug('Calling %s@%s with args:%.10240r. wire_context: %s cache_on: %s',
322 url, name, args, context_uid, cache_on)
322 url, name, args, context_uid, cache_on)
323 return _streaming_remote_call(url, payload, EXCEPTIONS_MAP, self._session, self.CHUNK_SIZE)
323 return _streaming_remote_call(url, payload, EXCEPTIONS_MAP, self._session, self.CHUNK_SIZE)
324
324
325 result = streaming_remote_call(cache_key)
325 result = streaming_remote_call(cache_key)
326 if self._call_with_logging:
326 if self._call_with_logging:
327 log.debug('Call %s@%s took: %.4fs. wire_context: %s',
327 log.debug('Call %s@%s took: %.4fs. wire_context: %s',
328 url, name, time.time()-start, context_uid)
328 url, name, time.time()-start, context_uid)
329 return result
329 return result
330
330
331 def __getitem__(self, key):
331 def __getitem__(self, key):
332 return self.revision(key)
332 return self.revision(key)
333
333
334 def _create_vcs_cache_context(self, *args):
334 def _create_vcs_cache_context(self, *args):
335 """
335 """
336 Creates a unique string which is passed to the VCSServer on every
336 Creates a unique string which is passed to the VCSServer on every
337 remote call. It is used as cache key in the VCSServer.
337 remote call. It is used as cache key in the VCSServer.
338 """
338 """
339 hash_key = '-'.join(map(str, args))
339 hash_key = '-'.join(map(str, args))
340 return str(uuid.uuid5(uuid.NAMESPACE_URL, hash_key))
340 return str(uuid.uuid5(uuid.NAMESPACE_URL, hash_key))
341
341
342 def invalidate_vcs_cache(self):
342 def invalidate_vcs_cache(self):
343 """
343 """
344 This invalidates the context which is sent to the VCSServer on every
344 This invalidates the context which is sent to the VCSServer on every
345 call to a remote method. It forces the VCSServer to create a fresh
345 call to a remote method. It forces the VCSServer to create a fresh
346 repository instance on the next call to a remote method.
346 repository instance on the next call to a remote method.
347 """
347 """
348 self._wire['context'] = str(uuid.uuid4())
348 self._wire['context'] = str(uuid.uuid4())
349
349
350
350
351 class VcsHttpProxy(object):
351 class VcsHttpProxy(object):
352
352
353 CHUNK_SIZE = 16384
353 CHUNK_SIZE = 16384
354
354
355 def __init__(self, server_and_port, backend_endpoint):
355 def __init__(self, server_and_port, backend_endpoint):
356 retries = Retry(total=5, connect=None, read=None, redirect=None)
356 retries = Retry(total=5, connect=None, read=None, redirect=None)
357
357
358 adapter = requests.adapters.HTTPAdapter(max_retries=retries)
358 adapter = requests.adapters.HTTPAdapter(max_retries=retries)
359 self.base_url = urllib.parse.urlparse.urljoin('http://%s' % server_and_port, backend_endpoint)
359 self.base_url = urllib.parse.urljoin('http://%s' % server_and_port, backend_endpoint)
360 self.session = requests.Session()
360 self.session = requests.Session()
361 self.session.mount('http://', adapter)
361 self.session.mount('http://', adapter)
362
362
363 def handle(self, environment, input_data, *args, **kwargs):
363 def handle(self, environment, input_data, *args, **kwargs):
364 data = {
364 data = {
365 'environment': environment,
365 'environment': environment,
366 'input_data': input_data,
366 'input_data': input_data,
367 'args': args,
367 'args': args,
368 'kwargs': kwargs
368 'kwargs': kwargs
369 }
369 }
370 result = self.session.post(
370 result = self.session.post(
371 self.base_url, msgpack.packb(data), stream=True)
371 self.base_url, msgpack.packb(data), stream=True)
372 return self._get_result(result)
372 return self._get_result(result)
373
373
374 def _deserialize_and_raise(self, error):
374 def _deserialize_and_raise(self, error):
375 exception = Exception(error['message'])
375 exception = Exception(error['message'])
376 try:
376 try:
377 exception._vcs_kind = error['_vcs_kind']
377 exception._vcs_kind = error['_vcs_kind']
378 except KeyError:
378 except KeyError:
379 pass
379 pass
380 raise exception
380 raise exception
381
381
382 def _iterate(self, result):
382 def _iterate(self, result):
383 unpacker = msgpack.Unpacker()
383 unpacker = msgpack.Unpacker()
384 for line in result.iter_content(chunk_size=self.CHUNK_SIZE):
384 for line in result.iter_content(chunk_size=self.CHUNK_SIZE):
385 unpacker.feed(line)
385 unpacker.feed(line)
386 for chunk in unpacker:
386 for chunk in unpacker:
387 yield chunk
387 yield chunk
388
388
389 def _get_result(self, result):
389 def _get_result(self, result):
390 iterator = self._iterate(result)
390 iterator = self._iterate(result)
391 error = next(iterator)
391 error = next(iterator)
392 if error:
392 if error:
393 self._deserialize_and_raise(error)
393 self._deserialize_and_raise(error)
394
394
395 status = next(iterator)
395 status = next(iterator)
396 headers = next(iterator)
396 headers = next(iterator)
397
397
398 return iterator, status, headers
398 return iterator, status, headers
399
399
400
400
401 class ThreadlocalSessionFactory(object):
401 class ThreadlocalSessionFactory(object):
402 """
402 """
403 Creates one CurlSession per thread on demand.
403 Creates one CurlSession per thread on demand.
404 """
404 """
405
405
406 def __init__(self):
406 def __init__(self):
407 self._thread_local = threading.local()
407 self._thread_local = threading.local()
408
408
409 def __call__(self):
409 def __call__(self):
410 if not hasattr(self._thread_local, 'curl_session'):
410 if not hasattr(self._thread_local, 'curl_session'):
411 self._thread_local.curl_session = CurlSession()
411 self._thread_local.curl_session = CurlSession()
412 return self._thread_local.curl_session
412 return self._thread_local.curl_session
@@ -1,140 +1,140 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2010-2020 RhodeCode GmbH
3 # Copyright (C) 2010-2020 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21 import pytest
21 import pytest
22 import urllib.parse
22 import urllib.parse
23 import mock
23 import mock
24 import simplejson as json
24 import simplejson as json
25
25
26 from rhodecode.lib.vcs.backends.base import Config
26 from rhodecode.lib.vcs.backends.base import Config
27 from rhodecode.tests.lib.middleware import mock_scm_app
27 from rhodecode.tests.lib.middleware import mock_scm_app
28 import rhodecode.lib.middleware.simplegit as simplegit
28 import rhodecode.lib.middleware.simplegit as simplegit
29
29
30
30
31 def get_environ(url, request_method):
31 def get_environ(url, request_method):
32 """Construct a minimum WSGI environ based on the URL."""
32 """Construct a minimum WSGI environ based on the URL."""
33 parsed_url = urllib.parse.urlparse.urlparse(url)
33 parsed_url = urllib.parse.urlparse(url)
34 environ = {
34 environ = {
35 'PATH_INFO': parsed_url.path,
35 'PATH_INFO': parsed_url.path,
36 'QUERY_STRING': parsed_url.query,
36 'QUERY_STRING': parsed_url.query,
37 'REQUEST_METHOD': request_method,
37 'REQUEST_METHOD': request_method,
38 }
38 }
39
39
40 return environ
40 return environ
41
41
42
42
43 @pytest.mark.parametrize(
43 @pytest.mark.parametrize(
44 'url, expected_action, request_method',
44 'url, expected_action, request_method',
45 [
45 [
46 ('/foo/bar/info/refs?service=git-upload-pack', 'pull', 'GET'),
46 ('/foo/bar/info/refs?service=git-upload-pack', 'pull', 'GET'),
47 ('/foo/bar/info/refs?service=git-receive-pack', 'push', 'GET'),
47 ('/foo/bar/info/refs?service=git-receive-pack', 'push', 'GET'),
48 ('/foo/bar/git-upload-pack', 'pull', 'GET'),
48 ('/foo/bar/git-upload-pack', 'pull', 'GET'),
49 ('/foo/bar/git-receive-pack', 'push', 'GET'),
49 ('/foo/bar/git-receive-pack', 'push', 'GET'),
50 # Edge case: missing data for info/refs
50 # Edge case: missing data for info/refs
51 ('/foo/info/refs?service=', 'pull', 'GET'),
51 ('/foo/info/refs?service=', 'pull', 'GET'),
52 ('/foo/info/refs', 'pull', 'GET'),
52 ('/foo/info/refs', 'pull', 'GET'),
53 # Edge case: git command comes with service argument
53 # Edge case: git command comes with service argument
54 ('/foo/git-upload-pack?service=git-receive-pack', 'pull', 'GET'),
54 ('/foo/git-upload-pack?service=git-receive-pack', 'pull', 'GET'),
55 ('/foo/git-receive-pack?service=git-upload-pack', 'push', 'GET'),
55 ('/foo/git-receive-pack?service=git-upload-pack', 'push', 'GET'),
56 # Edge case: repo name conflicts with git commands
56 # Edge case: repo name conflicts with git commands
57 ('/git-receive-pack/git-upload-pack', 'pull', 'GET'),
57 ('/git-receive-pack/git-upload-pack', 'pull', 'GET'),
58 ('/git-receive-pack/git-receive-pack', 'push', 'GET'),
58 ('/git-receive-pack/git-receive-pack', 'push', 'GET'),
59 ('/git-upload-pack/git-upload-pack', 'pull', 'GET'),
59 ('/git-upload-pack/git-upload-pack', 'pull', 'GET'),
60 ('/git-upload-pack/git-receive-pack', 'push', 'GET'),
60 ('/git-upload-pack/git-receive-pack', 'push', 'GET'),
61 ('/foo/git-receive-pack', 'push', 'GET'),
61 ('/foo/git-receive-pack', 'push', 'GET'),
62 # Edge case: not a smart protocol url
62 # Edge case: not a smart protocol url
63 ('/foo/bar', 'pull', 'GET'),
63 ('/foo/bar', 'pull', 'GET'),
64 # GIT LFS cases, batch
64 # GIT LFS cases, batch
65 ('/foo/bar/info/lfs/objects/batch', 'push', 'GET'),
65 ('/foo/bar/info/lfs/objects/batch', 'push', 'GET'),
66 ('/foo/bar/info/lfs/objects/batch', 'pull', 'POST'),
66 ('/foo/bar/info/lfs/objects/batch', 'pull', 'POST'),
67 # GIT LFS oid, dl/upl
67 # GIT LFS oid, dl/upl
68 ('/foo/bar/info/lfs/abcdeabcde', 'pull', 'GET'),
68 ('/foo/bar/info/lfs/abcdeabcde', 'pull', 'GET'),
69 ('/foo/bar/info/lfs/abcdeabcde', 'push', 'PUT'),
69 ('/foo/bar/info/lfs/abcdeabcde', 'push', 'PUT'),
70 ('/foo/bar/info/lfs/abcdeabcde', 'push', 'POST'),
70 ('/foo/bar/info/lfs/abcdeabcde', 'push', 'POST'),
71 # Edge case: repo name conflicts with git commands
71 # Edge case: repo name conflicts with git commands
72 ('/info/lfs/info/lfs/objects/batch', 'push', 'GET'),
72 ('/info/lfs/info/lfs/objects/batch', 'push', 'GET'),
73 ('/info/lfs/info/lfs/objects/batch', 'pull', 'POST'),
73 ('/info/lfs/info/lfs/objects/batch', 'pull', 'POST'),
74
74
75 ])
75 ])
76 def test_get_action(url, expected_action, request_method, baseapp, request_stub):
76 def test_get_action(url, expected_action, request_method, baseapp, request_stub):
77 app = simplegit.SimpleGit(config={'auth_ret_code': '', 'base_path': ''},
77 app = simplegit.SimpleGit(config={'auth_ret_code': '', 'base_path': ''},
78 registry=request_stub.registry)
78 registry=request_stub.registry)
79 assert expected_action == app._get_action(get_environ(url, request_method))
79 assert expected_action == app._get_action(get_environ(url, request_method))
80
80
81
81
82 @pytest.mark.parametrize(
82 @pytest.mark.parametrize(
83 'url, expected_repo_name, request_method',
83 'url, expected_repo_name, request_method',
84 [
84 [
85 ('/foo/info/refs?service=git-upload-pack', 'foo', 'GET'),
85 ('/foo/info/refs?service=git-upload-pack', 'foo', 'GET'),
86 ('/foo/bar/info/refs?service=git-receive-pack', 'foo/bar', 'GET'),
86 ('/foo/bar/info/refs?service=git-receive-pack', 'foo/bar', 'GET'),
87 ('/foo/git-upload-pack', 'foo', 'GET'),
87 ('/foo/git-upload-pack', 'foo', 'GET'),
88 ('/foo/git-receive-pack', 'foo', 'GET'),
88 ('/foo/git-receive-pack', 'foo', 'GET'),
89 ('/foo/bar/git-upload-pack', 'foo/bar', 'GET'),
89 ('/foo/bar/git-upload-pack', 'foo/bar', 'GET'),
90 ('/foo/bar/git-receive-pack', 'foo/bar', 'GET'),
90 ('/foo/bar/git-receive-pack', 'foo/bar', 'GET'),
91
91
92 # GIT LFS cases, batch
92 # GIT LFS cases, batch
93 ('/foo/bar/info/lfs/objects/batch', 'foo/bar', 'GET'),
93 ('/foo/bar/info/lfs/objects/batch', 'foo/bar', 'GET'),
94 ('/example-git/info/lfs/objects/batch', 'example-git', 'POST'),
94 ('/example-git/info/lfs/objects/batch', 'example-git', 'POST'),
95 # GIT LFS oid, dl/upl
95 # GIT LFS oid, dl/upl
96 ('/foo/info/lfs/abcdeabcde', 'foo', 'GET'),
96 ('/foo/info/lfs/abcdeabcde', 'foo', 'GET'),
97 ('/foo/bar/info/lfs/abcdeabcde', 'foo/bar', 'PUT'),
97 ('/foo/bar/info/lfs/abcdeabcde', 'foo/bar', 'PUT'),
98 ('/my-git-repo/info/lfs/abcdeabcde', 'my-git-repo', 'POST'),
98 ('/my-git-repo/info/lfs/abcdeabcde', 'my-git-repo', 'POST'),
99 # Edge case: repo name conflicts with git commands
99 # Edge case: repo name conflicts with git commands
100 ('/info/lfs/info/lfs/objects/batch', 'info/lfs', 'GET'),
100 ('/info/lfs/info/lfs/objects/batch', 'info/lfs', 'GET'),
101 ('/info/lfs/info/lfs/objects/batch', 'info/lfs', 'POST'),
101 ('/info/lfs/info/lfs/objects/batch', 'info/lfs', 'POST'),
102
102
103 ])
103 ])
104 def test_get_repository_name(url, expected_repo_name, request_method, baseapp, request_stub):
104 def test_get_repository_name(url, expected_repo_name, request_method, baseapp, request_stub):
105 app = simplegit.SimpleGit(config={'auth_ret_code': '', 'base_path': ''},
105 app = simplegit.SimpleGit(config={'auth_ret_code': '', 'base_path': ''},
106 registry=request_stub.registry)
106 registry=request_stub.registry)
107 assert expected_repo_name == app._get_repository_name(
107 assert expected_repo_name == app._get_repository_name(
108 get_environ(url, request_method))
108 get_environ(url, request_method))
109
109
110
110
111 def test_get_config(user_util, baseapp, request_stub):
111 def test_get_config(user_util, baseapp, request_stub):
112 repo = user_util.create_repo(repo_type='git')
112 repo = user_util.create_repo(repo_type='git')
113 app = simplegit.SimpleGit(config={'auth_ret_code': '', 'base_path': ''},
113 app = simplegit.SimpleGit(config={'auth_ret_code': '', 'base_path': ''},
114 registry=request_stub.registry)
114 registry=request_stub.registry)
115 extras = {'foo': 'FOO', 'bar': 'BAR'}
115 extras = {'foo': 'FOO', 'bar': 'BAR'}
116
116
117 # We copy the extras as the method below will change the contents.
117 # We copy the extras as the method below will change the contents.
118 git_config = app._create_config(dict(extras), repo_name=repo.repo_name)
118 git_config = app._create_config(dict(extras), repo_name=repo.repo_name)
119
119
120 expected_config = dict(extras)
120 expected_config = dict(extras)
121 expected_config.update({
121 expected_config.update({
122 'git_update_server_info': False,
122 'git_update_server_info': False,
123 'git_lfs_enabled': False,
123 'git_lfs_enabled': False,
124 'git_lfs_store_path': git_config['git_lfs_store_path'],
124 'git_lfs_store_path': git_config['git_lfs_store_path'],
125 'git_lfs_http_scheme': 'http'
125 'git_lfs_http_scheme': 'http'
126 })
126 })
127
127
128 assert git_config == expected_config
128 assert git_config == expected_config
129
129
130
130
131 def test_create_wsgi_app_uses_scm_app_from_simplevcs(baseapp, request_stub):
131 def test_create_wsgi_app_uses_scm_app_from_simplevcs(baseapp, request_stub):
132 config = {
132 config = {
133 'auth_ret_code': '',
133 'auth_ret_code': '',
134 'base_path': '',
134 'base_path': '',
135 'vcs.scm_app_implementation':
135 'vcs.scm_app_implementation':
136 'rhodecode.tests.lib.middleware.mock_scm_app',
136 'rhodecode.tests.lib.middleware.mock_scm_app',
137 }
137 }
138 app = simplegit.SimpleGit(config=config, registry=request_stub.registry)
138 app = simplegit.SimpleGit(config=config, registry=request_stub.registry)
139 wsgi_app = app._create_wsgi_app('/tmp/test', 'test_repo', {})
139 wsgi_app = app._create_wsgi_app('/tmp/test', 'test_repo', {})
140 assert wsgi_app is mock_scm_app.mock_git_wsgi
140 assert wsgi_app is mock_scm_app.mock_git_wsgi
@@ -1,156 +1,156 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2010-2020 RhodeCode GmbH
3 # Copyright (C) 2010-2020 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21 import urllib.parse
21 import urllib.parse
22
22
23 import mock
23 import mock
24 import pytest
24 import pytest
25 import simplejson as json
25 import simplejson as json
26
26
27 from rhodecode.lib.vcs.backends.base import Config
27 from rhodecode.lib.vcs.backends.base import Config
28 from rhodecode.tests.lib.middleware import mock_scm_app
28 from rhodecode.tests.lib.middleware import mock_scm_app
29 import rhodecode.lib.middleware.simplehg as simplehg
29 import rhodecode.lib.middleware.simplehg as simplehg
30
30
31
31
32 def get_environ(url):
32 def get_environ(url):
33 """Construct a minimum WSGI environ based on the URL."""
33 """Construct a minimum WSGI environ based on the URL."""
34 parsed_url = urllib.parse.urlparse.urlparse(url)
34 parsed_url = urllib.parse.urlparse(url)
35 environ = {
35 environ = {
36 'PATH_INFO': parsed_url.path,
36 'PATH_INFO': parsed_url.path,
37 'QUERY_STRING': parsed_url.query,
37 'QUERY_STRING': parsed_url.query,
38 }
38 }
39
39
40 return environ
40 return environ
41
41
42
42
43 @pytest.mark.parametrize(
43 @pytest.mark.parametrize(
44 'url, expected_action',
44 'url, expected_action',
45 [
45 [
46 ('/foo/bar?cmd=unbundle&key=tip', 'push'),
46 ('/foo/bar?cmd=unbundle&key=tip', 'push'),
47 ('/foo/bar?cmd=pushkey&key=tip', 'push'),
47 ('/foo/bar?cmd=pushkey&key=tip', 'push'),
48 ('/foo/bar?cmd=listkeys&key=tip', 'pull'),
48 ('/foo/bar?cmd=listkeys&key=tip', 'pull'),
49 ('/foo/bar?cmd=changegroup&key=tip', 'pull'),
49 ('/foo/bar?cmd=changegroup&key=tip', 'pull'),
50 ('/foo/bar?cmd=hello', 'pull'),
50 ('/foo/bar?cmd=hello', 'pull'),
51 ('/foo/bar?cmd=batch', 'push'),
51 ('/foo/bar?cmd=batch', 'push'),
52 ('/foo/bar?cmd=putlfile', 'push'),
52 ('/foo/bar?cmd=putlfile', 'push'),
53 # Edge case: unknown argument: assume push
53 # Edge case: unknown argument: assume push
54 ('/foo/bar?cmd=unknown&key=tip', 'push'),
54 ('/foo/bar?cmd=unknown&key=tip', 'push'),
55 ('/foo/bar?cmd=&key=tip', 'push'),
55 ('/foo/bar?cmd=&key=tip', 'push'),
56 # Edge case: not cmd argument
56 # Edge case: not cmd argument
57 ('/foo/bar?key=tip', 'push'),
57 ('/foo/bar?key=tip', 'push'),
58 ])
58 ])
59 def test_get_action(url, expected_action, request_stub):
59 def test_get_action(url, expected_action, request_stub):
60 app = simplehg.SimpleHg(config={'auth_ret_code': '', 'base_path': ''},
60 app = simplehg.SimpleHg(config={'auth_ret_code': '', 'base_path': ''},
61 registry=request_stub.registry)
61 registry=request_stub.registry)
62 assert expected_action == app._get_action(get_environ(url))
62 assert expected_action == app._get_action(get_environ(url))
63
63
64
64
65 @pytest.mark.parametrize(
65 @pytest.mark.parametrize(
66 'environ, expected_xargs, expected_batch',
66 'environ, expected_xargs, expected_batch',
67 [
67 [
68 ({},
68 ({},
69 [''], ['push']),
69 [''], ['push']),
70
70
71 ({'HTTP_X_HGARG_1': ''},
71 ({'HTTP_X_HGARG_1': ''},
72 [''], ['push']),
72 [''], ['push']),
73
73
74 ({'HTTP_X_HGARG_1': 'cmds=listkeys+namespace%3Dphases'},
74 ({'HTTP_X_HGARG_1': 'cmds=listkeys+namespace%3Dphases'},
75 ['listkeys namespace=phases'], ['pull']),
75 ['listkeys namespace=phases'], ['pull']),
76
76
77 ({'HTTP_X_HGARG_1': 'cmds=pushkey+namespace%3Dbookmarks%2Ckey%3Dbm%2Cold%3D%2Cnew%3Dcb9a9f314b8b07ba71012fcdbc544b5a4d82ff5b'},
77 ({'HTTP_X_HGARG_1': 'cmds=pushkey+namespace%3Dbookmarks%2Ckey%3Dbm%2Cold%3D%2Cnew%3Dcb9a9f314b8b07ba71012fcdbc544b5a4d82ff5b'},
78 ['pushkey namespace=bookmarks,key=bm,old=,new=cb9a9f314b8b07ba71012fcdbc544b5a4d82ff5b'], ['push']),
78 ['pushkey namespace=bookmarks,key=bm,old=,new=cb9a9f314b8b07ba71012fcdbc544b5a4d82ff5b'], ['push']),
79
79
80 ({'HTTP_X_HGARG_1': 'namespace=phases'},
80 ({'HTTP_X_HGARG_1': 'namespace=phases'},
81 ['namespace=phases'], ['push']),
81 ['namespace=phases'], ['push']),
82
82
83 ])
83 ])
84 def test_xarg_and_batch_commands(environ, expected_xargs, expected_batch):
84 def test_xarg_and_batch_commands(environ, expected_xargs, expected_batch):
85 app = simplehg.SimpleHg
85 app = simplehg.SimpleHg
86
86
87 result = app._get_xarg_headers(environ)
87 result = app._get_xarg_headers(environ)
88 result_batch = app._get_batch_cmd(environ)
88 result_batch = app._get_batch_cmd(environ)
89 assert expected_xargs == result
89 assert expected_xargs == result
90 assert expected_batch == result_batch
90 assert expected_batch == result_batch
91
91
92
92
93 @pytest.mark.parametrize(
93 @pytest.mark.parametrize(
94 'url, expected_repo_name',
94 'url, expected_repo_name',
95 [
95 [
96 ('/foo?cmd=unbundle&key=tip', 'foo'),
96 ('/foo?cmd=unbundle&key=tip', 'foo'),
97 ('/foo/bar?cmd=pushkey&key=tip', 'foo/bar'),
97 ('/foo/bar?cmd=pushkey&key=tip', 'foo/bar'),
98 ('/foo/bar/baz?cmd=listkeys&key=tip', 'foo/bar/baz'),
98 ('/foo/bar/baz?cmd=listkeys&key=tip', 'foo/bar/baz'),
99 # Repos with trailing slashes.
99 # Repos with trailing slashes.
100 ('/foo/?cmd=unbundle&key=tip', 'foo'),
100 ('/foo/?cmd=unbundle&key=tip', 'foo'),
101 ('/foo/bar/?cmd=pushkey&key=tip', 'foo/bar'),
101 ('/foo/bar/?cmd=pushkey&key=tip', 'foo/bar'),
102 ('/foo/bar/baz/?cmd=listkeys&key=tip', 'foo/bar/baz'),
102 ('/foo/bar/baz/?cmd=listkeys&key=tip', 'foo/bar/baz'),
103 ])
103 ])
104 def test_get_repository_name(url, expected_repo_name, request_stub):
104 def test_get_repository_name(url, expected_repo_name, request_stub):
105 app = simplehg.SimpleHg(config={'auth_ret_code': '', 'base_path': ''},
105 app = simplehg.SimpleHg(config={'auth_ret_code': '', 'base_path': ''},
106 registry=request_stub.registry)
106 registry=request_stub.registry)
107 assert expected_repo_name == app._get_repository_name(get_environ(url))
107 assert expected_repo_name == app._get_repository_name(get_environ(url))
108
108
109
109
110 def test_get_config(user_util, baseapp, request_stub):
110 def test_get_config(user_util, baseapp, request_stub):
111 repo = user_util.create_repo(repo_type='git')
111 repo = user_util.create_repo(repo_type='git')
112 app = simplehg.SimpleHg(config={'auth_ret_code': '', 'base_path': ''},
112 app = simplehg.SimpleHg(config={'auth_ret_code': '', 'base_path': ''},
113 registry=request_stub.registry)
113 registry=request_stub.registry)
114 extras = [('foo', 'FOO', 'bar', 'BAR')]
114 extras = [('foo', 'FOO', 'bar', 'BAR')]
115
115
116 hg_config = app._create_config(extras, repo_name=repo.repo_name)
116 hg_config = app._create_config(extras, repo_name=repo.repo_name)
117
117
118 config = simplehg.utils.make_db_config(repo=repo.repo_name)
118 config = simplehg.utils.make_db_config(repo=repo.repo_name)
119 config.set('rhodecode', 'RC_SCM_DATA', json.dumps(extras))
119 config.set('rhodecode', 'RC_SCM_DATA', json.dumps(extras))
120 hg_config_org = config
120 hg_config_org = config
121
121
122 expected_config = [
122 expected_config = [
123 ('vcs_svn_tag', 'ff89f8c714d135d865f44b90e5413b88de19a55f', '/tags/*'),
123 ('vcs_svn_tag', 'ff89f8c714d135d865f44b90e5413b88de19a55f', '/tags/*'),
124 ('web', 'push_ssl', 'False'),
124 ('web', 'push_ssl', 'False'),
125 ('web', 'allow_push', '*'),
125 ('web', 'allow_push', '*'),
126 ('web', 'allow_archive', 'gz zip bz2'),
126 ('web', 'allow_archive', 'gz zip bz2'),
127 ('web', 'baseurl', '/'),
127 ('web', 'baseurl', '/'),
128 ('vcs_git_lfs', 'store_location', hg_config_org.get('vcs_git_lfs', 'store_location')),
128 ('vcs_git_lfs', 'store_location', hg_config_org.get('vcs_git_lfs', 'store_location')),
129 ('vcs_svn_branch', '9aac1a38c3b8a0cdc4ae0f960a5f83332bc4fa5e', '/branches/*'),
129 ('vcs_svn_branch', '9aac1a38c3b8a0cdc4ae0f960a5f83332bc4fa5e', '/branches/*'),
130 ('vcs_svn_branch', 'c7e6a611c87da06529fd0dd733308481d67c71a8', '/trunk'),
130 ('vcs_svn_branch', 'c7e6a611c87da06529fd0dd733308481d67c71a8', '/trunk'),
131 ('largefiles', 'usercache', hg_config_org.get('largefiles', 'usercache')),
131 ('largefiles', 'usercache', hg_config_org.get('largefiles', 'usercache')),
132 ('hooks', 'preoutgoing.pre_pull', 'python:vcsserver.hooks.pre_pull'),
132 ('hooks', 'preoutgoing.pre_pull', 'python:vcsserver.hooks.pre_pull'),
133 ('hooks', 'prechangegroup.pre_push', 'python:vcsserver.hooks.pre_push'),
133 ('hooks', 'prechangegroup.pre_push', 'python:vcsserver.hooks.pre_push'),
134 ('hooks', 'outgoing.pull_logger', 'python:vcsserver.hooks.log_pull_action'),
134 ('hooks', 'outgoing.pull_logger', 'python:vcsserver.hooks.log_pull_action'),
135 ('hooks', 'pretxnchangegroup.pre_push', 'python:vcsserver.hooks.pre_push'),
135 ('hooks', 'pretxnchangegroup.pre_push', 'python:vcsserver.hooks.pre_push'),
136 ('hooks', 'changegroup.push_logger', 'python:vcsserver.hooks.log_push_action'),
136 ('hooks', 'changegroup.push_logger', 'python:vcsserver.hooks.log_push_action'),
137 ('hooks', 'changegroup.repo_size', 'python:vcsserver.hooks.repo_size'),
137 ('hooks', 'changegroup.repo_size', 'python:vcsserver.hooks.repo_size'),
138 ('phases', 'publish', 'True'),
138 ('phases', 'publish', 'True'),
139 ('extensions', 'largefiles', ''),
139 ('extensions', 'largefiles', ''),
140 ('paths', '/', hg_config_org.get('paths', '/')),
140 ('paths', '/', hg_config_org.get('paths', '/')),
141 ('rhodecode', 'RC_SCM_DATA', '[["foo", "FOO", "bar", "BAR"]]')
141 ('rhodecode', 'RC_SCM_DATA', '[["foo", "FOO", "bar", "BAR"]]')
142 ]
142 ]
143 for entry in expected_config:
143 for entry in expected_config:
144 assert entry in hg_config
144 assert entry in hg_config
145
145
146
146
147 def test_create_wsgi_app_uses_scm_app_from_simplevcs(request_stub):
147 def test_create_wsgi_app_uses_scm_app_from_simplevcs(request_stub):
148 config = {
148 config = {
149 'auth_ret_code': '',
149 'auth_ret_code': '',
150 'base_path': '',
150 'base_path': '',
151 'vcs.scm_app_implementation':
151 'vcs.scm_app_implementation':
152 'rhodecode.tests.lib.middleware.mock_scm_app',
152 'rhodecode.tests.lib.middleware.mock_scm_app',
153 }
153 }
154 app = simplehg.SimpleHg(config=config, registry=request_stub.registry)
154 app = simplehg.SimpleHg(config=config, registry=request_stub.registry)
155 wsgi_app = app._create_wsgi_app('/tmp/test', 'test_repo', {})
155 wsgi_app = app._create_wsgi_app('/tmp/test', 'test_repo', {})
156 assert wsgi_app is mock_scm_app.mock_hg_wsgi
156 assert wsgi_app is mock_scm_app.mock_hg_wsgi
@@ -1,468 +1,467 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2010-2020 RhodeCode GmbH
3 # Copyright (C) 2010-2020 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21 import threading
21 import threading
22 import time
22 import time
23 import logging
23 import logging
24 import os.path
24 import os.path
25 import subprocess
25 import subprocess
26 import tempfile
26 import tempfile
27 import urllib.request, urllib.error, urllib.parse
27 import urllib.request, urllib.error, urllib.parse
28 from lxml.html import fromstring, tostring
28 from lxml.html import fromstring, tostring
29 from lxml.cssselect import CSSSelector
29 from lxml.cssselect import CSSSelector
30 import urllib.parse.urlparse
31 from urllib.parse import unquote_plus
30 from urllib.parse import unquote_plus
32 import webob
31 import webob
33
32
34 from webtest.app import TestResponse, TestApp
33 from webtest.app import TestResponse, TestApp
35 from webtest.compat import print_stderr
34 from webtest.compat import print_stderr
36
35
37 import pytest
36 import pytest
38 import rc_testdata
37 import rc_testdata
39
38
40 from rhodecode.model.db import User, Repository
39 from rhodecode.model.db import User, Repository
41 from rhodecode.model.meta import Session
40 from rhodecode.model.meta import Session
42 from rhodecode.model.scm import ScmModel
41 from rhodecode.model.scm import ScmModel
43 from rhodecode.lib.vcs.backends.svn.repository import SubversionRepository
42 from rhodecode.lib.vcs.backends.svn.repository import SubversionRepository
44 from rhodecode.lib.vcs.backends.base import EmptyCommit
43 from rhodecode.lib.vcs.backends.base import EmptyCommit
45 from rhodecode.tests import login_user_session
44 from rhodecode.tests import login_user_session
46
45
47 log = logging.getLogger(__name__)
46 log = logging.getLogger(__name__)
48
47
49
48
50 class CustomTestResponse(TestResponse):
49 class CustomTestResponse(TestResponse):
51
50
52 def _save_output(self, out):
51 def _save_output(self, out):
53 f = tempfile.NamedTemporaryFile(delete=False, prefix='rc-test-', suffix='.html')
52 f = tempfile.NamedTemporaryFile(delete=False, prefix='rc-test-', suffix='.html')
54 f.write(out)
53 f.write(out)
55 return f.name
54 return f.name
56
55
57 def mustcontain(self, *strings, **kw):
56 def mustcontain(self, *strings, **kw):
58 """
57 """
59 Assert that the response contains all of the strings passed
58 Assert that the response contains all of the strings passed
60 in as arguments.
59 in as arguments.
61
60
62 Equivalent to::
61 Equivalent to::
63
62
64 assert string in res
63 assert string in res
65 """
64 """
66 print_body = kw.pop('print_body', False)
65 print_body = kw.pop('print_body', False)
67 if 'no' in kw:
66 if 'no' in kw:
68 no = kw['no']
67 no = kw['no']
69 del kw['no']
68 del kw['no']
70 if isinstance(no, str):
69 if isinstance(no, str):
71 no = [no]
70 no = [no]
72 else:
71 else:
73 no = []
72 no = []
74 if kw:
73 if kw:
75 raise TypeError(
74 raise TypeError(
76 "The only keyword argument allowed is 'no' got %s" % kw)
75 "The only keyword argument allowed is 'no' got %s" % kw)
77
76
78 f = self._save_output(str(self))
77 f = self._save_output(str(self))
79
78
80 for s in strings:
79 for s in strings:
81 if not s in self:
80 if not s in self:
82 print_stderr("Actual response (no %r):" % s)
81 print_stderr("Actual response (no %r):" % s)
83 print_stderr("body output saved as `%s`" % f)
82 print_stderr("body output saved as `%s`" % f)
84 if print_body:
83 if print_body:
85 print_stderr(str(self))
84 print_stderr(str(self))
86 raise IndexError(
85 raise IndexError(
87 "Body does not contain string %r, body output saved as %s" % (s, f))
86 "Body does not contain string %r, body output saved as %s" % (s, f))
88
87
89 for no_s in no:
88 for no_s in no:
90 if no_s in self:
89 if no_s in self:
91 print_stderr("Actual response (has %r)" % no_s)
90 print_stderr("Actual response (has %r)" % no_s)
92 print_stderr("body output saved as `%s`" % f)
91 print_stderr("body output saved as `%s`" % f)
93 if print_body:
92 if print_body:
94 print_stderr(str(self))
93 print_stderr(str(self))
95 raise IndexError(
94 raise IndexError(
96 "Body contains bad string %r, body output saved as %s" % (no_s, f))
95 "Body contains bad string %r, body output saved as %s" % (no_s, f))
97
96
98 def assert_response(self):
97 def assert_response(self):
99 return AssertResponse(self)
98 return AssertResponse(self)
100
99
101 def get_session_from_response(self):
100 def get_session_from_response(self):
102 """
101 """
103 This returns the session from a response object.
102 This returns the session from a response object.
104 """
103 """
105 from rhodecode.lib.rc_beaker import session_factory_from_settings
104 from rhodecode.lib.rc_beaker import session_factory_from_settings
106 session = session_factory_from_settings(self.test_app._pyramid_settings)
105 session = session_factory_from_settings(self.test_app._pyramid_settings)
107 return session(self.request)
106 return session(self.request)
108
107
109
108
110 class TestRequest(webob.BaseRequest):
109 class TestRequest(webob.BaseRequest):
111
110
112 # for py.test
111 # for py.test
113 disabled = True
112 disabled = True
114 ResponseClass = CustomTestResponse
113 ResponseClass = CustomTestResponse
115
114
116 def add_response_callback(self, callback):
115 def add_response_callback(self, callback):
117 pass
116 pass
118
117
119
118
120 class CustomTestApp(TestApp):
119 class CustomTestApp(TestApp):
121 """
120 """
122 Custom app to make mustcontain more Useful, and extract special methods
121 Custom app to make mustcontain more Useful, and extract special methods
123 """
122 """
124 RequestClass = TestRequest
123 RequestClass = TestRequest
125 rc_login_data = {}
124 rc_login_data = {}
126 rc_current_session = None
125 rc_current_session = None
127
126
128 def login(self, username=None, password=None):
127 def login(self, username=None, password=None):
129 from rhodecode.lib import auth
128 from rhodecode.lib import auth
130
129
131 if username and password:
130 if username and password:
132 session = login_user_session(self, username, password)
131 session = login_user_session(self, username, password)
133 else:
132 else:
134 session = login_user_session(self)
133 session = login_user_session(self)
135
134
136 self.rc_login_data['csrf_token'] = auth.get_csrf_token(session)
135 self.rc_login_data['csrf_token'] = auth.get_csrf_token(session)
137 self.rc_current_session = session
136 self.rc_current_session = session
138 return session['rhodecode_user']
137 return session['rhodecode_user']
139
138
140 @property
139 @property
141 def csrf_token(self):
140 def csrf_token(self):
142 return self.rc_login_data['csrf_token']
141 return self.rc_login_data['csrf_token']
143
142
144 @property
143 @property
145 def _pyramid_registry(self):
144 def _pyramid_registry(self):
146 return self.app.config.registry
145 return self.app.config.registry
147
146
148 @property
147 @property
149 def _pyramid_settings(self):
148 def _pyramid_settings(self):
150 return self._pyramid_registry.settings
149 return self._pyramid_registry.settings
151
150
152
151
153 def set_anonymous_access(enabled):
152 def set_anonymous_access(enabled):
154 """(Dis)allows anonymous access depending on parameter `enabled`"""
153 """(Dis)allows anonymous access depending on parameter `enabled`"""
155 user = User.get_default_user()
154 user = User.get_default_user()
156 user.active = enabled
155 user.active = enabled
157 Session().add(user)
156 Session().add(user)
158 Session().commit()
157 Session().commit()
159 time.sleep(1.5) # must sleep for cache (1s to expire)
158 time.sleep(1.5) # must sleep for cache (1s to expire)
160 log.info('anonymous access is now: %s', enabled)
159 log.info('anonymous access is now: %s', enabled)
161 assert enabled == User.get_default_user().active, (
160 assert enabled == User.get_default_user().active, (
162 'Cannot set anonymous access')
161 'Cannot set anonymous access')
163
162
164
163
165 def check_xfail_backends(node, backend_alias):
164 def check_xfail_backends(node, backend_alias):
166 # Using "xfail_backends" here intentionally, since this marks work
165 # Using "xfail_backends" here intentionally, since this marks work
167 # which is "to be done" soon.
166 # which is "to be done" soon.
168 skip_marker = node.get_closest_marker('xfail_backends')
167 skip_marker = node.get_closest_marker('xfail_backends')
169 if skip_marker and backend_alias in skip_marker.args:
168 if skip_marker and backend_alias in skip_marker.args:
170 msg = "Support for backend %s to be developed." % (backend_alias, )
169 msg = "Support for backend %s to be developed." % (backend_alias, )
171 msg = skip_marker.kwargs.get('reason', msg)
170 msg = skip_marker.kwargs.get('reason', msg)
172 pytest.xfail(msg)
171 pytest.xfail(msg)
173
172
174
173
175 def check_skip_backends(node, backend_alias):
174 def check_skip_backends(node, backend_alias):
176 # Using "skip_backends" here intentionally, since this marks work which is
175 # Using "skip_backends" here intentionally, since this marks work which is
177 # not supported.
176 # not supported.
178 skip_marker = node.get_closest_marker('skip_backends')
177 skip_marker = node.get_closest_marker('skip_backends')
179 if skip_marker and backend_alias in skip_marker.args:
178 if skip_marker and backend_alias in skip_marker.args:
180 msg = "Feature not supported for backend %s." % (backend_alias, )
179 msg = "Feature not supported for backend %s." % (backend_alias, )
181 msg = skip_marker.kwargs.get('reason', msg)
180 msg = skip_marker.kwargs.get('reason', msg)
182 pytest.skip(msg)
181 pytest.skip(msg)
183
182
184
183
185 def extract_git_repo_from_dump(dump_name, repo_name):
184 def extract_git_repo_from_dump(dump_name, repo_name):
186 """Create git repo `repo_name` from dump `dump_name`."""
185 """Create git repo `repo_name` from dump `dump_name`."""
187 repos_path = ScmModel().repos_path
186 repos_path = ScmModel().repos_path
188 target_path = os.path.join(repos_path, repo_name)
187 target_path = os.path.join(repos_path, repo_name)
189 rc_testdata.extract_git_dump(dump_name, target_path)
188 rc_testdata.extract_git_dump(dump_name, target_path)
190 return target_path
189 return target_path
191
190
192
191
193 def extract_hg_repo_from_dump(dump_name, repo_name):
192 def extract_hg_repo_from_dump(dump_name, repo_name):
194 """Create hg repo `repo_name` from dump `dump_name`."""
193 """Create hg repo `repo_name` from dump `dump_name`."""
195 repos_path = ScmModel().repos_path
194 repos_path = ScmModel().repos_path
196 target_path = os.path.join(repos_path, repo_name)
195 target_path = os.path.join(repos_path, repo_name)
197 rc_testdata.extract_hg_dump(dump_name, target_path)
196 rc_testdata.extract_hg_dump(dump_name, target_path)
198 return target_path
197 return target_path
199
198
200
199
201 def extract_svn_repo_from_dump(dump_name, repo_name):
200 def extract_svn_repo_from_dump(dump_name, repo_name):
202 """Create a svn repo `repo_name` from dump `dump_name`."""
201 """Create a svn repo `repo_name` from dump `dump_name`."""
203 repos_path = ScmModel().repos_path
202 repos_path = ScmModel().repos_path
204 target_path = os.path.join(repos_path, repo_name)
203 target_path = os.path.join(repos_path, repo_name)
205 SubversionRepository(target_path, create=True)
204 SubversionRepository(target_path, create=True)
206 _load_svn_dump_into_repo(dump_name, target_path)
205 _load_svn_dump_into_repo(dump_name, target_path)
207 return target_path
206 return target_path
208
207
209
208
210 def assert_message_in_log(log_records, message, levelno, module):
209 def assert_message_in_log(log_records, message, levelno, module):
211 messages = [
210 messages = [
212 r.message for r in log_records
211 r.message for r in log_records
213 if r.module == module and r.levelno == levelno
212 if r.module == module and r.levelno == levelno
214 ]
213 ]
215 assert message in messages
214 assert message in messages
216
215
217
216
218 def _load_svn_dump_into_repo(dump_name, repo_path):
217 def _load_svn_dump_into_repo(dump_name, repo_path):
219 """
218 """
220 Utility to populate a svn repository with a named dump
219 Utility to populate a svn repository with a named dump
221
220
222 Currently the dumps are in rc_testdata. They might later on be
221 Currently the dumps are in rc_testdata. They might later on be
223 integrated with the main repository once they stabilize more.
222 integrated with the main repository once they stabilize more.
224 """
223 """
225 dump = rc_testdata.load_svn_dump(dump_name)
224 dump = rc_testdata.load_svn_dump(dump_name)
226 load_dump = subprocess.Popen(
225 load_dump = subprocess.Popen(
227 ['svnadmin', 'load', repo_path],
226 ['svnadmin', 'load', repo_path],
228 stdin=subprocess.PIPE, stdout=subprocess.PIPE,
227 stdin=subprocess.PIPE, stdout=subprocess.PIPE,
229 stderr=subprocess.PIPE)
228 stderr=subprocess.PIPE)
230 out, err = load_dump.communicate(dump)
229 out, err = load_dump.communicate(dump)
231 if load_dump.returncode != 0:
230 if load_dump.returncode != 0:
232 log.error("Output of load_dump command: %s", out)
231 log.error("Output of load_dump command: %s", out)
233 log.error("Error output of load_dump command: %s", err)
232 log.error("Error output of load_dump command: %s", err)
234 raise Exception(
233 raise Exception(
235 'Failed to load dump "%s" into repository at path "%s".'
234 'Failed to load dump "%s" into repository at path "%s".'
236 % (dump_name, repo_path))
235 % (dump_name, repo_path))
237
236
238
237
239 class AssertResponse(object):
238 class AssertResponse(object):
240 """
239 """
241 Utility that helps to assert things about a given HTML response.
240 Utility that helps to assert things about a given HTML response.
242 """
241 """
243
242
244 def __init__(self, response):
243 def __init__(self, response):
245 self.response = response
244 self.response = response
246
245
247 def get_imports(self):
246 def get_imports(self):
248 return fromstring, tostring, CSSSelector
247 return fromstring, tostring, CSSSelector
249
248
250 def one_element_exists(self, css_selector):
249 def one_element_exists(self, css_selector):
251 self.get_element(css_selector)
250 self.get_element(css_selector)
252
251
253 def no_element_exists(self, css_selector):
252 def no_element_exists(self, css_selector):
254 assert not self._get_elements(css_selector)
253 assert not self._get_elements(css_selector)
255
254
256 def element_equals_to(self, css_selector, expected_content):
255 def element_equals_to(self, css_selector, expected_content):
257 element = self.get_element(css_selector)
256 element = self.get_element(css_selector)
258 element_text = self._element_to_string(element)
257 element_text = self._element_to_string(element)
259 assert expected_content in element_text
258 assert expected_content in element_text
260
259
261 def element_contains(self, css_selector, expected_content):
260 def element_contains(self, css_selector, expected_content):
262 element = self.get_element(css_selector)
261 element = self.get_element(css_selector)
263 assert expected_content in element.text_content()
262 assert expected_content in element.text_content()
264
263
265 def element_value_contains(self, css_selector, expected_content):
264 def element_value_contains(self, css_selector, expected_content):
266 element = self.get_element(css_selector)
265 element = self.get_element(css_selector)
267 assert expected_content in element.value
266 assert expected_content in element.value
268
267
269 def contains_one_link(self, link_text, href):
268 def contains_one_link(self, link_text, href):
270 fromstring, tostring, CSSSelector = self.get_imports()
269 fromstring, tostring, CSSSelector = self.get_imports()
271 doc = fromstring(self.response.body)
270 doc = fromstring(self.response.body)
272 sel = CSSSelector('a[href]')
271 sel = CSSSelector('a[href]')
273 elements = [
272 elements = [
274 e for e in sel(doc) if e.text_content().strip() == link_text]
273 e for e in sel(doc) if e.text_content().strip() == link_text]
275 assert len(elements) == 1, "Did not find link or found multiple links"
274 assert len(elements) == 1, "Did not find link or found multiple links"
276 self._ensure_url_equal(elements[0].attrib.get('href'), href)
275 self._ensure_url_equal(elements[0].attrib.get('href'), href)
277
276
278 def contains_one_anchor(self, anchor_id):
277 def contains_one_anchor(self, anchor_id):
279 fromstring, tostring, CSSSelector = self.get_imports()
278 fromstring, tostring, CSSSelector = self.get_imports()
280 doc = fromstring(self.response.body)
279 doc = fromstring(self.response.body)
281 sel = CSSSelector('#' + anchor_id)
280 sel = CSSSelector('#' + anchor_id)
282 elements = sel(doc)
281 elements = sel(doc)
283 assert len(elements) == 1, 'cannot find 1 element {}'.format(anchor_id)
282 assert len(elements) == 1, 'cannot find 1 element {}'.format(anchor_id)
284
283
285 def _ensure_url_equal(self, found, expected):
284 def _ensure_url_equal(self, found, expected):
286 assert _Url(found) == _Url(expected)
285 assert _Url(found) == _Url(expected)
287
286
288 def get_element(self, css_selector):
287 def get_element(self, css_selector):
289 elements = self._get_elements(css_selector)
288 elements = self._get_elements(css_selector)
290 assert len(elements) == 1, 'cannot find 1 element {}'.format(css_selector)
289 assert len(elements) == 1, 'cannot find 1 element {}'.format(css_selector)
291 return elements[0]
290 return elements[0]
292
291
293 def get_elements(self, css_selector):
292 def get_elements(self, css_selector):
294 return self._get_elements(css_selector)
293 return self._get_elements(css_selector)
295
294
296 def _get_elements(self, css_selector):
295 def _get_elements(self, css_selector):
297 fromstring, tostring, CSSSelector = self.get_imports()
296 fromstring, tostring, CSSSelector = self.get_imports()
298 doc = fromstring(self.response.body)
297 doc = fromstring(self.response.body)
299 sel = CSSSelector(css_selector)
298 sel = CSSSelector(css_selector)
300 elements = sel(doc)
299 elements = sel(doc)
301 return elements
300 return elements
302
301
303 def _element_to_string(self, element):
302 def _element_to_string(self, element):
304 fromstring, tostring, CSSSelector = self.get_imports()
303 fromstring, tostring, CSSSelector = self.get_imports()
305 return tostring(element)
304 return tostring(element)
306
305
307
306
308 class _Url(object):
307 class _Url(object):
309 """
308 """
310 A url object that can be compared with other url orbjects
309 A url object that can be compared with other url orbjects
311 without regard to the vagaries of encoding, escaping, and ordering
310 without regard to the vagaries of encoding, escaping, and ordering
312 of parameters in query strings.
311 of parameters in query strings.
313
312
314 Inspired by
313 Inspired by
315 http://stackoverflow.com/questions/5371992/comparing-two-urls-in-python
314 http://stackoverflow.com/questions/5371992/comparing-two-urls-in-python
316 """
315 """
317
316
318 def __init__(self, url):
317 def __init__(self, url):
319 parts = urllib.parse.urlparse(url)
318 parts = urllib.parse.urlparse(url)
320 _query = frozenset(urllib.parse.parse_qsl(parts.query))
319 _query = frozenset(urllib.parse.parse_qsl(parts.query))
321 _path = unquote_plus(parts.path)
320 _path = unquote_plus(parts.path)
322 parts = parts._replace(query=_query, path=_path)
321 parts = parts._replace(query=_query, path=_path)
323 self.parts = parts
322 self.parts = parts
324
323
325 def __eq__(self, other):
324 def __eq__(self, other):
326 return self.parts == other.parts
325 return self.parts == other.parts
327
326
328 def __hash__(self):
327 def __hash__(self):
329 return hash(self.parts)
328 return hash(self.parts)
330
329
331
330
332 def run_test_concurrently(times, raise_catched_exc=True):
331 def run_test_concurrently(times, raise_catched_exc=True):
333 """
332 """
334 Add this decorator to small pieces of code that you want to test
333 Add this decorator to small pieces of code that you want to test
335 concurrently
334 concurrently
336
335
337 ex:
336 ex:
338
337
339 @test_concurrently(25)
338 @test_concurrently(25)
340 def my_test_function():
339 def my_test_function():
341 ...
340 ...
342 """
341 """
343 def test_concurrently_decorator(test_func):
342 def test_concurrently_decorator(test_func):
344 def wrapper(*args, **kwargs):
343 def wrapper(*args, **kwargs):
345 exceptions = []
344 exceptions = []
346
345
347 def call_test_func():
346 def call_test_func():
348 try:
347 try:
349 test_func(*args, **kwargs)
348 test_func(*args, **kwargs)
350 except Exception as e:
349 except Exception as e:
351 exceptions.append(e)
350 exceptions.append(e)
352 if raise_catched_exc:
351 if raise_catched_exc:
353 raise
352 raise
354 threads = []
353 threads = []
355 for i in range(times):
354 for i in range(times):
356 threads.append(threading.Thread(target=call_test_func))
355 threads.append(threading.Thread(target=call_test_func))
357 for t in threads:
356 for t in threads:
358 t.start()
357 t.start()
359 for t in threads:
358 for t in threads:
360 t.join()
359 t.join()
361 if exceptions:
360 if exceptions:
362 raise Exception(
361 raise Exception(
363 'test_concurrently intercepted %s exceptions: %s' % (
362 'test_concurrently intercepted %s exceptions: %s' % (
364 len(exceptions), exceptions))
363 len(exceptions), exceptions))
365 return wrapper
364 return wrapper
366 return test_concurrently_decorator
365 return test_concurrently_decorator
367
366
368
367
369 def wait_for_url(url, timeout=10):
368 def wait_for_url(url, timeout=10):
370 """
369 """
371 Wait until URL becomes reachable.
370 Wait until URL becomes reachable.
372
371
373 It polls the URL until the timeout is reached or it became reachable.
372 It polls the URL until the timeout is reached or it became reachable.
374 If will call to `py.test.fail` in case the URL is not reachable.
373 If will call to `py.test.fail` in case the URL is not reachable.
375 """
374 """
376 timeout = time.time() + timeout
375 timeout = time.time() + timeout
377 last = 0
376 last = 0
378 wait = 0.1
377 wait = 0.1
379
378
380 while timeout > last:
379 while timeout > last:
381 last = time.time()
380 last = time.time()
382 if is_url_reachable(url):
381 if is_url_reachable(url):
383 break
382 break
384 elif (last + wait) > time.time():
383 elif (last + wait) > time.time():
385 # Go to sleep because not enough time has passed since last check.
384 # Go to sleep because not enough time has passed since last check.
386 time.sleep(wait)
385 time.sleep(wait)
387 else:
386 else:
388 pytest.fail("Timeout while waiting for URL {}".format(url))
387 pytest.fail("Timeout while waiting for URL {}".format(url))
389
388
390
389
391 def is_url_reachable(url):
390 def is_url_reachable(url):
392 try:
391 try:
393 urllib.request.urlopen(url)
392 urllib.request.urlopen(url)
394 except urllib.error.URLError:
393 except urllib.error.URLError:
395 log.exception('URL `{}` reach error'.format(url))
394 log.exception('URL `{}` reach error'.format(url))
396 return False
395 return False
397 return True
396 return True
398
397
399
398
400 def repo_on_filesystem(repo_name):
399 def repo_on_filesystem(repo_name):
401 from rhodecode.lib import vcs
400 from rhodecode.lib import vcs
402 from rhodecode.tests import TESTS_TMP_PATH
401 from rhodecode.tests import TESTS_TMP_PATH
403 repo = vcs.get_vcs_instance(
402 repo = vcs.get_vcs_instance(
404 os.path.join(TESTS_TMP_PATH, repo_name), create=False)
403 os.path.join(TESTS_TMP_PATH, repo_name), create=False)
405 return repo is not None
404 return repo is not None
406
405
407
406
408 def commit_change(
407 def commit_change(
409 repo, filename, content, message, vcs_type, parent=None, newfile=False):
408 repo, filename, content, message, vcs_type, parent=None, newfile=False):
410 from rhodecode.tests import TEST_USER_ADMIN_LOGIN
409 from rhodecode.tests import TEST_USER_ADMIN_LOGIN
411
410
412 repo = Repository.get_by_repo_name(repo)
411 repo = Repository.get_by_repo_name(repo)
413 _commit = parent
412 _commit = parent
414 if not parent:
413 if not parent:
415 _commit = EmptyCommit(alias=vcs_type)
414 _commit = EmptyCommit(alias=vcs_type)
416
415
417 if newfile:
416 if newfile:
418 nodes = {
417 nodes = {
419 filename: {
418 filename: {
420 'content': content
419 'content': content
421 }
420 }
422 }
421 }
423 commit = ScmModel().create_nodes(
422 commit = ScmModel().create_nodes(
424 user=TEST_USER_ADMIN_LOGIN, repo=repo,
423 user=TEST_USER_ADMIN_LOGIN, repo=repo,
425 message=message,
424 message=message,
426 nodes=nodes,
425 nodes=nodes,
427 parent_commit=_commit,
426 parent_commit=_commit,
428 author='{} <admin@rhodecode.com>'.format(TEST_USER_ADMIN_LOGIN),
427 author='{} <admin@rhodecode.com>'.format(TEST_USER_ADMIN_LOGIN),
429 )
428 )
430 else:
429 else:
431 commit = ScmModel().commit_change(
430 commit = ScmModel().commit_change(
432 repo=repo.scm_instance(), repo_name=repo.repo_name,
431 repo=repo.scm_instance(), repo_name=repo.repo_name,
433 commit=parent, user=TEST_USER_ADMIN_LOGIN,
432 commit=parent, user=TEST_USER_ADMIN_LOGIN,
434 author='{} <admin@rhodecode.com>'.format(TEST_USER_ADMIN_LOGIN),
433 author='{} <admin@rhodecode.com>'.format(TEST_USER_ADMIN_LOGIN),
435 message=message,
434 message=message,
436 content=content,
435 content=content,
437 f_path=filename
436 f_path=filename
438 )
437 )
439 return commit
438 return commit
440
439
441
440
442 def permission_update_data_generator(csrf_token, default=None, grant=None, revoke=None):
441 def permission_update_data_generator(csrf_token, default=None, grant=None, revoke=None):
443 if not default:
442 if not default:
444 raise ValueError('Permission for default user must be given')
443 raise ValueError('Permission for default user must be given')
445 form_data = [(
444 form_data = [(
446 'csrf_token', csrf_token
445 'csrf_token', csrf_token
447 )]
446 )]
448 # add default
447 # add default
449 form_data.extend([
448 form_data.extend([
450 ('u_perm_1', default)
449 ('u_perm_1', default)
451 ])
450 ])
452
451
453 if grant:
452 if grant:
454 for cnt, (obj_id, perm, obj_name, obj_type) in enumerate(grant, 1):
453 for cnt, (obj_id, perm, obj_name, obj_type) in enumerate(grant, 1):
455 form_data.extend([
454 form_data.extend([
456 ('perm_new_member_perm_new{}'.format(cnt), perm),
455 ('perm_new_member_perm_new{}'.format(cnt), perm),
457 ('perm_new_member_id_new{}'.format(cnt), obj_id),
456 ('perm_new_member_id_new{}'.format(cnt), obj_id),
458 ('perm_new_member_name_new{}'.format(cnt), obj_name),
457 ('perm_new_member_name_new{}'.format(cnt), obj_name),
459 ('perm_new_member_type_new{}'.format(cnt), obj_type),
458 ('perm_new_member_type_new{}'.format(cnt), obj_type),
460
459
461 ])
460 ])
462 if revoke:
461 if revoke:
463 for obj_id, obj_type in revoke:
462 for obj_id, obj_type in revoke:
464 form_data.extend([
463 form_data.extend([
465 ('perm_del_member_id_{}'.format(obj_id), obj_id),
464 ('perm_del_member_id_{}'.format(obj_id), obj_id),
466 ('perm_del_member_type_{}'.format(obj_id), obj_type),
465 ('perm_del_member_type_{}'.format(obj_id), obj_type),
467 ])
466 ])
468 return form_data
467 return form_data
General Comments 0
You need to be logged in to leave comments. Login now