##// END OF EJS Templates
mercurial: expose HGUSER into environ becuase some extensions explicitly rely on this....
marcink -
r353:2a24bd1c default
parent child Browse files
Show More
@@ -1,476 +1,478 b''
1 1 # RhodeCode VCSServer provides access to different vcs backends via network.
2 2 # Copyright (C) 2014-2018 RhodeCode GmbH
3 3 #
4 4 # This program is free software; you can redistribute it and/or modify
5 5 # it under the terms of the GNU General Public License as published by
6 6 # the Free Software Foundation; either version 3 of the License, or
7 7 # (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software Foundation,
16 16 # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
17 17
18 18 import base64
19 19 import locale
20 20 import logging
21 21 import uuid
22 22 import wsgiref.util
23 23 import traceback
24 24 from itertools import chain
25 25
26 26 import simplejson as json
27 27 import msgpack
28 28 from beaker.cache import CacheManager
29 29 from beaker.util import parse_cache_config_options
30 30 from pyramid.config import Configurator
31 31 from pyramid.wsgi import wsgiapp
32 32
33 33 from vcsserver import remote_wsgi, scm_app, settings, hgpatches
34 34 from vcsserver.git_lfs.app import GIT_LFS_CONTENT_TYPE, GIT_LFS_PROTO_PAT
35 35 from vcsserver.echo_stub import remote_wsgi as remote_wsgi_stub
36 36 from vcsserver.echo_stub.echo_app import EchoApp
37 37 from vcsserver.exceptions import HTTPRepoLocked
38 38 from vcsserver.server import VcsServer
39 39
40 40 try:
41 41 from vcsserver.git import GitFactory, GitRemote
42 42 except ImportError:
43 43 GitFactory = None
44 44 GitRemote = None
45 45
46 46 try:
47 47 from vcsserver.hg import MercurialFactory, HgRemote
48 48 except ImportError:
49 49 MercurialFactory = None
50 50 HgRemote = None
51 51
52 52 try:
53 53 from vcsserver.svn import SubversionFactory, SvnRemote
54 54 except ImportError:
55 55 SubversionFactory = None
56 56 SvnRemote = None
57 57
58 58 log = logging.getLogger(__name__)
59 59
60 60
61 61 def _is_request_chunked(environ):
62 62 stream = environ.get('HTTP_TRANSFER_ENCODING', '') == 'chunked'
63 63 return stream
64 64
65 65
66 66 class VCS(object):
67 67 def __init__(self, locale=None, cache_config=None):
68 68 self.locale = locale
69 69 self.cache_config = cache_config
70 70 self._configure_locale()
71 71 self._initialize_cache()
72 72
73 73 if GitFactory and GitRemote:
74 74 git_repo_cache = self.cache.get_cache_region(
75 75 'git', region='repo_object')
76 76 git_factory = GitFactory(git_repo_cache)
77 77 self._git_remote = GitRemote(git_factory)
78 78 else:
79 79 log.info("Git client import failed")
80 80
81 81 if MercurialFactory and HgRemote:
82 82 hg_repo_cache = self.cache.get_cache_region(
83 83 'hg', region='repo_object')
84 84 hg_factory = MercurialFactory(hg_repo_cache)
85 85 self._hg_remote = HgRemote(hg_factory)
86 86 else:
87 87 log.info("Mercurial client import failed")
88 88
89 89 if SubversionFactory and SvnRemote:
90 90 svn_repo_cache = self.cache.get_cache_region(
91 91 'svn', region='repo_object')
92 92 svn_factory = SubversionFactory(svn_repo_cache)
93 93 self._svn_remote = SvnRemote(svn_factory, hg_factory=hg_factory)
94 94 else:
95 95 log.info("Subversion client import failed")
96 96
97 97 self._vcsserver = VcsServer()
98 98
99 99 def _initialize_cache(self):
100 100 cache_config = parse_cache_config_options(self.cache_config)
101 101 log.info('Initializing beaker cache: %s' % cache_config)
102 102 self.cache = CacheManager(**cache_config)
103 103
104 104 def _configure_locale(self):
105 105 if self.locale:
106 106 log.info('Settings locale: `LC_ALL` to %s' % self.locale)
107 107 else:
108 108 log.info(
109 109 'Configuring locale subsystem based on environment variables')
110 110 try:
111 111 # If self.locale is the empty string, then the locale
112 112 # module will use the environment variables. See the
113 113 # documentation of the package `locale`.
114 114 locale.setlocale(locale.LC_ALL, self.locale)
115 115
116 116 language_code, encoding = locale.getlocale()
117 117 log.info(
118 118 'Locale set to language code "%s" with encoding "%s".',
119 119 language_code, encoding)
120 120 except locale.Error:
121 121 log.exception(
122 122 'Cannot set locale, not configuring the locale system')
123 123
124 124
125 125 class WsgiProxy(object):
126 126 def __init__(self, wsgi):
127 127 self.wsgi = wsgi
128 128
129 129 def __call__(self, environ, start_response):
130 130 input_data = environ['wsgi.input'].read()
131 131 input_data = msgpack.unpackb(input_data)
132 132
133 133 error = None
134 134 try:
135 135 data, status, headers = self.wsgi.handle(
136 136 input_data['environment'], input_data['input_data'],
137 137 *input_data['args'], **input_data['kwargs'])
138 138 except Exception as e:
139 139 data, status, headers = [], None, None
140 140 error = {
141 141 'message': str(e),
142 142 '_vcs_kind': getattr(e, '_vcs_kind', None)
143 143 }
144 144
145 145 start_response(200, {})
146 146 return self._iterator(error, status, headers, data)
147 147
148 148 def _iterator(self, error, status, headers, data):
149 149 initial_data = [
150 150 error,
151 151 status,
152 152 headers,
153 153 ]
154 154
155 155 for d in chain(initial_data, data):
156 156 yield msgpack.packb(d)
157 157
158 158
159 159 class HTTPApplication(object):
160 160 ALLOWED_EXCEPTIONS = ('KeyError', 'URLError')
161 161
162 162 remote_wsgi = remote_wsgi
163 163 _use_echo_app = False
164 164
165 165 def __init__(self, settings=None, global_config=None):
166 166 self.config = Configurator(settings=settings)
167 167 self.global_config = global_config
168 168
169 169 locale = settings.get('locale', '') or 'en_US.UTF-8'
170 170 vcs = VCS(locale=locale, cache_config=settings)
171 171 self._remotes = {
172 172 'hg': vcs._hg_remote,
173 173 'git': vcs._git_remote,
174 174 'svn': vcs._svn_remote,
175 175 'server': vcs._vcsserver,
176 176 }
177 177 if settings.get('dev.use_echo_app', 'false').lower() == 'true':
178 178 self._use_echo_app = True
179 179 log.warning("Using EchoApp for VCS operations.")
180 180 self.remote_wsgi = remote_wsgi_stub
181 181 self._configure_settings(settings)
182 182 self._configure()
183 183
184 184 def _configure_settings(self, app_settings):
185 185 """
186 186 Configure the settings module.
187 187 """
188 188 git_path = app_settings.get('git_path', None)
189 189 if git_path:
190 190 settings.GIT_EXECUTABLE = git_path
191 191
192 192 def _configure(self):
193 193 self.config.add_renderer(
194 194 name='msgpack',
195 195 factory=self._msgpack_renderer_factory)
196 196
197 197 self.config.add_route('service', '/_service')
198 198 self.config.add_route('status', '/status')
199 199 self.config.add_route('hg_proxy', '/proxy/hg')
200 200 self.config.add_route('git_proxy', '/proxy/git')
201 201 self.config.add_route('vcs', '/{backend}')
202 202 self.config.add_route('stream_git', '/stream/git/*repo_name')
203 203 self.config.add_route('stream_hg', '/stream/hg/*repo_name')
204 204
205 205 self.config.add_view(
206 206 self.status_view, route_name='status', renderer='json')
207 207 self.config.add_view(
208 208 self.service_view, route_name='service', renderer='msgpack')
209 209
210 210 self.config.add_view(self.hg_proxy(), route_name='hg_proxy')
211 211 self.config.add_view(self.git_proxy(), route_name='git_proxy')
212 212 self.config.add_view(
213 213 self.vcs_view, route_name='vcs', renderer='msgpack',
214 214 custom_predicates=[self.is_vcs_view])
215 215
216 216 self.config.add_view(self.hg_stream(), route_name='stream_hg')
217 217 self.config.add_view(self.git_stream(), route_name='stream_git')
218 218
219 219 def notfound(request):
220 220 return {'status': '404 NOT FOUND'}
221 221 self.config.add_notfound_view(notfound, renderer='json')
222 222
223 223 self.config.add_view(self.handle_vcs_exception, context=Exception)
224 224
225 225 self.config.add_tween(
226 226 'vcsserver.tweens.RequestWrapperTween',
227 227 )
228 228
229 229 def wsgi_app(self):
230 230 return self.config.make_wsgi_app()
231 231
232 232 def vcs_view(self, request):
233 233 remote = self._remotes[request.matchdict['backend']]
234 234 payload = msgpack.unpackb(request.body, use_list=True)
235 235 method = payload.get('method')
236 236 params = payload.get('params')
237 237 wire = params.get('wire')
238 238 args = params.get('args')
239 239 kwargs = params.get('kwargs')
240 240 if wire:
241 241 try:
242 242 wire['context'] = uuid.UUID(wire['context'])
243 243 except KeyError:
244 244 pass
245 245 args.insert(0, wire)
246 246
247 247 log.debug('method called:%s with kwargs:%s', method, kwargs)
248 248 try:
249 249 resp = getattr(remote, method)(*args, **kwargs)
250 250 except Exception as e:
251 251 tb_info = traceback.format_exc()
252 252
253 253 type_ = e.__class__.__name__
254 254 if type_ not in self.ALLOWED_EXCEPTIONS:
255 255 type_ = None
256 256
257 257 resp = {
258 258 'id': payload.get('id'),
259 259 'error': {
260 260 'message': e.message,
261 261 'traceback': tb_info,
262 262 'type': type_
263 263 }
264 264 }
265 265 try:
266 266 resp['error']['_vcs_kind'] = e._vcs_kind
267 267 except AttributeError:
268 268 pass
269 269 else:
270 270 resp = {
271 271 'id': payload.get('id'),
272 272 'result': resp
273 273 }
274 274
275 275 return resp
276 276
277 277 def status_view(self, request):
278 278 import vcsserver
279 279 return {'status': 'OK', 'vcsserver_version': vcsserver.__version__}
280 280
281 281 def service_view(self, request):
282 282 import vcsserver
283 283 import ConfigParser as configparser
284 284
285 285 payload = msgpack.unpackb(request.body, use_list=True)
286 286
287 287 try:
288 288 path = self.global_config['__file__']
289 289 config = configparser.ConfigParser()
290 290 config.read(path)
291 291 parsed_ini = config
292 292 if parsed_ini.has_section('server:main'):
293 293 parsed_ini = dict(parsed_ini.items('server:main'))
294 294 except Exception:
295 295 log.exception('Failed to read .ini file for display')
296 296 parsed_ini = {}
297 297
298 298 resp = {
299 299 'id': payload.get('id'),
300 300 'result': dict(
301 301 version=vcsserver.__version__,
302 302 config=parsed_ini,
303 303 payload=payload,
304 304 )
305 305 }
306 306 return resp
307 307
308 308 def _msgpack_renderer_factory(self, info):
309 309 def _render(value, system):
310 310 value = msgpack.packb(value)
311 311 request = system.get('request')
312 312 if request is not None:
313 313 response = request.response
314 314 ct = response.content_type
315 315 if ct == response.default_content_type:
316 316 response.content_type = 'application/x-msgpack'
317 317 return value
318 318 return _render
319 319
320 320 def set_env_from_config(self, environ, config):
321 321 dict_conf = {}
322 322 try:
323 323 for elem in config:
324 324 if elem[0] == 'rhodecode':
325 325 dict_conf = json.loads(elem[2])
326 326 break
327 327 except Exception:
328 328 log.exception('Failed to fetch SCM CONFIG')
329 329 return
330 330
331 331 username = dict_conf.get('username')
332 332 if username:
333 333 environ['REMOTE_USER'] = username
334 # mercurial specific, some extension api rely on this
335 environ['HGUSER'] = username
334 336
335 337 ip = dict_conf.get('ip')
336 338 if ip:
337 339 environ['REMOTE_HOST'] = ip
338 340
339 341 if _is_request_chunked(environ):
340 342 # set the compatibility flag for webob
341 343 environ['wsgi.input_terminated'] = True
342 344
343 345 def hg_proxy(self):
344 346 @wsgiapp
345 347 def _hg_proxy(environ, start_response):
346 348 app = WsgiProxy(self.remote_wsgi.HgRemoteWsgi())
347 349 return app(environ, start_response)
348 350 return _hg_proxy
349 351
350 352 def git_proxy(self):
351 353 @wsgiapp
352 354 def _git_proxy(environ, start_response):
353 355 app = WsgiProxy(self.remote_wsgi.GitRemoteWsgi())
354 356 return app(environ, start_response)
355 357 return _git_proxy
356 358
357 359 def hg_stream(self):
358 360 if self._use_echo_app:
359 361 @wsgiapp
360 362 def _hg_stream(environ, start_response):
361 363 app = EchoApp('fake_path', 'fake_name', None)
362 364 return app(environ, start_response)
363 365 return _hg_stream
364 366 else:
365 367 @wsgiapp
366 368 def _hg_stream(environ, start_response):
367 369 log.debug('http-app: handling hg stream')
368 370 repo_path = environ['HTTP_X_RC_REPO_PATH']
369 371 repo_name = environ['HTTP_X_RC_REPO_NAME']
370 372 packed_config = base64.b64decode(
371 373 environ['HTTP_X_RC_REPO_CONFIG'])
372 374 config = msgpack.unpackb(packed_config)
373 375 app = scm_app.create_hg_wsgi_app(
374 376 repo_path, repo_name, config)
375 377
376 378 # Consistent path information for hgweb
377 379 environ['PATH_INFO'] = environ['HTTP_X_RC_PATH_INFO']
378 380 environ['REPO_NAME'] = repo_name
379 381 self.set_env_from_config(environ, config)
380 382
381 383 log.debug('http-app: starting app handler '
382 384 'with %s and process request', app)
383 385 return app(environ, ResponseFilter(start_response))
384 386 return _hg_stream
385 387
386 388 def git_stream(self):
387 389 if self._use_echo_app:
388 390 @wsgiapp
389 391 def _git_stream(environ, start_response):
390 392 app = EchoApp('fake_path', 'fake_name', None)
391 393 return app(environ, start_response)
392 394 return _git_stream
393 395 else:
394 396 @wsgiapp
395 397 def _git_stream(environ, start_response):
396 398 log.debug('http-app: handling git stream')
397 399 repo_path = environ['HTTP_X_RC_REPO_PATH']
398 400 repo_name = environ['HTTP_X_RC_REPO_NAME']
399 401 packed_config = base64.b64decode(
400 402 environ['HTTP_X_RC_REPO_CONFIG'])
401 403 config = msgpack.unpackb(packed_config)
402 404
403 405 environ['PATH_INFO'] = environ['HTTP_X_RC_PATH_INFO']
404 406 self.set_env_from_config(environ, config)
405 407
406 408 content_type = environ.get('CONTENT_TYPE', '')
407 409
408 410 path = environ['PATH_INFO']
409 411 is_lfs_request = GIT_LFS_CONTENT_TYPE in content_type
410 412 log.debug(
411 413 'LFS: Detecting if request `%s` is LFS server path based '
412 414 'on content type:`%s`, is_lfs:%s',
413 415 path, content_type, is_lfs_request)
414 416
415 417 if not is_lfs_request:
416 418 # fallback detection by path
417 419 if GIT_LFS_PROTO_PAT.match(path):
418 420 is_lfs_request = True
419 421 log.debug(
420 422 'LFS: fallback detection by path of: `%s`, is_lfs:%s',
421 423 path, is_lfs_request)
422 424
423 425 if is_lfs_request:
424 426 app = scm_app.create_git_lfs_wsgi_app(
425 427 repo_path, repo_name, config)
426 428 else:
427 429 app = scm_app.create_git_wsgi_app(
428 430 repo_path, repo_name, config)
429 431
430 432 log.debug('http-app: starting app handler '
431 433 'with %s and process request', app)
432 434
433 435 return app(environ, start_response)
434 436
435 437 return _git_stream
436 438
437 439 def is_vcs_view(self, context, request):
438 440 """
439 441 View predicate that returns true if given backend is supported by
440 442 defined remotes.
441 443 """
442 444 backend = request.matchdict.get('backend')
443 445 return backend in self._remotes
444 446
445 447 def handle_vcs_exception(self, exception, request):
446 448 _vcs_kind = getattr(exception, '_vcs_kind', '')
447 449 if _vcs_kind == 'repo_locked':
448 450 # Get custom repo-locked status code if present.
449 451 status_code = request.headers.get('X-RC-Locked-Status-Code')
450 452 return HTTPRepoLocked(
451 453 title=exception.message, status_code=status_code)
452 454
453 455 # Re-raise exception if we can not handle it.
454 456 log.exception(
455 457 'error occurred handling this request for path: %s', request.path)
456 458 raise exception
457 459
458 460
459 461 class ResponseFilter(object):
460 462
461 463 def __init__(self, start_response):
462 464 self._start_response = start_response
463 465
464 466 def __call__(self, status, response_headers, exc_info=None):
465 467 headers = tuple(
466 468 (h, v) for h, v in response_headers
467 469 if not wsgiref.util.is_hop_by_hop(h))
468 470 return self._start_response(status, headers, exc_info)
469 471
470 472
471 473 def main(global_config, **settings):
472 474 if MercurialFactory:
473 475 hgpatches.patch_largefiles_capabilities()
474 476 hgpatches.patch_subrepo_type_mapping()
475 477 app = HTTPApplication(settings=settings, global_config=global_config)
476 478 return app.wsgi_app()
General Comments 0
You need to be logged in to leave comments. Login now