##// END OF EJS Templates
binary envelope: add support for custom response type when using wrapped binary data in envelope
super-admin -
r1107:eadcb91a python3
parent child Browse files
Show More
@@ -1,178 +1,178 b''
1 1 # RhodeCode VCSServer provides access to different vcs backends via network.
2 2 # Copyright (C) 2014-2020 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 import os
18 18 import sys
19 19 import traceback
20 20 import logging
21 21 import urllib.parse
22 22
23 23 from vcsserver.lib.rc_cache import region_meta
24 24
25 25 from vcsserver import exceptions
26 26 from vcsserver.exceptions import NoContentException
27 27 from vcsserver.hgcompat import archival
28 28 from vcsserver.str_utils import safe_bytes
29 29
30 30 log = logging.getLogger(__name__)
31 31
32 32
33 33 class RepoFactory(object):
34 34 """
35 35 Utility to create instances of repository
36 36
37 37 It provides internal caching of the `repo` object based on
38 38 the :term:`call context`.
39 39 """
40 40 repo_type = None
41 41
42 42 def __init__(self):
43 43 self._cache_region = region_meta.dogpile_cache_regions['repo_object']
44 44
45 45 def _create_config(self, path, config):
46 46 config = {}
47 47 return config
48 48
49 49 def _create_repo(self, wire, create):
50 50 raise NotImplementedError()
51 51
52 52 def repo(self, wire, create=False):
53 53 raise NotImplementedError()
54 54
55 55
56 56 def obfuscate_qs(query_string):
57 57 if query_string is None:
58 58 return None
59 59
60 60 parsed = []
61 61 for k, v in urllib.parse.parse_qsl(query_string, keep_blank_values=True):
62 62 if k in ['auth_token', 'api_key']:
63 63 v = "*****"
64 64 parsed.append((k, v))
65 65
66 66 return '&'.join('{}{}'.format(
67 67 k, '={}'.format(v) if v else '') for k, v in parsed)
68 68
69 69
70 70 def raise_from_original(new_type, org_exc: Exception):
71 71 """
72 72 Raise a new exception type with original args and traceback.
73 73 """
74 74
75 75 exc_type, exc_value, exc_traceback = sys.exc_info()
76 76 new_exc = new_type(*exc_value.args)
77 77
78 78 # store the original traceback into the new exc
79 79 new_exc._org_exc_tb = traceback.format_tb(exc_traceback)
80 80
81 81 try:
82 82 raise new_exc.with_traceback(exc_traceback)
83 83 finally:
84 84 del exc_traceback
85 85
86 86
87 87 class ArchiveNode(object):
88 88 def __init__(self, path, mode, is_link, raw_bytes):
89 89 self.path = path
90 90 self.mode = mode
91 91 self.is_link = is_link
92 92 self.raw_bytes = raw_bytes
93 93
94 94
95 95 def archive_repo(walker, archive_dest_path, kind, mtime, archive_at_path,
96 96 archive_dir_name, commit_id, write_metadata=True, extra_metadata=None):
97 97 """
98 98 walker should be a file walker, for example:
99 99 def walker():
100 100 for file_info in files:
101 101 yield ArchiveNode(fn, mode, is_link, ctx[fn].data)
102 102 """
103 103 extra_metadata = extra_metadata or {}
104 104 archive_dest_path = safe_bytes(archive_dest_path)
105 105
106 106 if kind == "tgz":
107 107 archiver = archival.tarit(archive_dest_path, mtime, b"gz")
108 108 elif kind == "tbz2":
109 109 archiver = archival.tarit(archive_dest_path, mtime, b"bz2")
110 110 elif kind == 'zip':
111 111 archiver = archival.zipit(archive_dest_path, mtime)
112 112 else:
113 113 raise exceptions.ArchiveException()(
114 114 f'Remote does not support: "{kind}" archive type.')
115 115
116 116 for f in walker(commit_id, archive_at_path):
117 117 f_path = os.path.join(safe_bytes(archive_dir_name), safe_bytes(f.path).lstrip(b'/'))
118 118 try:
119 119 archiver.addfile(f_path, f.mode, f.is_link, f.raw_bytes())
120 120 except NoContentException:
121 121 # NOTE(marcink): this is a special case for SVN so we can create "empty"
122 122 # directories which arent supported by archiver
123 123 archiver.addfile(os.path.join(f_path, b'.dir'), f.mode, f.is_link, b'')
124 124
125 125 if write_metadata:
126 126 metadata = dict([
127 127 ('commit_id', commit_id),
128 128 ('mtime', mtime),
129 129 ])
130 130 metadata.update(extra_metadata)
131 131
132 132 meta = [safe_bytes(f"{f_name}:{value}") for f_name, value in metadata.items()]
133 133 f_path = os.path.join(safe_bytes(archive_dir_name), b'.archival.txt')
134 134 archiver.addfile(f_path, 0o644, False, b'\n'.join(meta))
135 135
136 136 return archiver.done()
137 137
138 138
139 139 class BinaryEnvelope(object):
140 def __init__(self, value, bin_type=True):
140 def __init__(self, value: bytes, bin_type=True):
141 141 self.value = value
142 142 self.bin_type = bin_type
143 143
144 144 def __len__(self):
145 145 return len(self.value)
146 146
147 147 def __getitem__(self, index):
148 148 return self.value[index]
149 149
150 150 def __iter__(self):
151 151 return iter(self.value)
152 152
153 153 def __str__(self):
154 154 return str(self.value)
155 155
156 156 def __repr__(self):
157 157 return repr(self.value)
158 158
159 159 def __eq__(self, other):
160 160 if isinstance(other, BinaryEnvelope):
161 161 return self.value == other.value
162 162 return False
163 163
164 164 def __ne__(self, other):
165 165 return not self.__eq__(other)
166 166
167 167 def __add__(self, other):
168 168 if isinstance(other, BinaryEnvelope):
169 169 return BinaryEnvelope(self.value + other.value)
170 170 raise TypeError(f"unsupported operand type(s) for +: 'BinaryEnvelope' and '{type(other)}'")
171 171
172 172 def __radd__(self, other):
173 173 if isinstance(other, BinaryEnvelope):
174 174 return BinaryEnvelope(other.value + self.value)
175 175 raise TypeError(f"unsupported operand type(s) for +: '{type(other)}' and 'BinaryEnvelope'")
176 176
177 177
178 178
@@ -1,87 +1,88 b''
1 1 # RhodeCode VCSServer provides access to different vcs backends via network.
2 2 # Copyright (C) 2014-2020 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 """
19 19 Mercurial libs compatibility
20 20 """
21 21
22 22 import mercurial
23 23 from mercurial import demandimport
24 24
25 25 # patch demandimport, due to bug in mercurial when it always triggers
26 26 # demandimport.enable()
27 27 from vcsserver.str_utils import safe_bytes
28 28
29 29 demandimport.enable = lambda *args, **kwargs: 1
30 30
31 31 from mercurial import ui
32 32 from mercurial import patch
33 33 from mercurial import config
34 34 from mercurial import extensions
35 35 from mercurial import scmutil
36 36 from mercurial import archival
37 37 from mercurial import discovery
38 38 from mercurial import unionrepo
39 39 from mercurial import localrepo
40 40 from mercurial import merge as hg_merge
41 41 from mercurial import subrepo
42 42 from mercurial import subrepoutil
43 43 from mercurial import tags as hg_tag
44 44 from mercurial import util as hgutil
45 45 from mercurial.commands import clone, pull
46 46 from mercurial.node import nullid
47 47 from mercurial.context import memctx, memfilectx
48 48 from mercurial.error import (
49 49 LookupError, RepoError, RepoLookupError, Abort, InterventionRequired,
50 50 RequirementError, ProgrammingError)
51 51 from mercurial.hgweb import hgweb_mod
52 52 from mercurial.localrepo import instance
53 53 from mercurial.match import match, alwaysmatcher, patternmatcher
54 54 from mercurial.mdiff import diffopts
55 55 from mercurial.node import bin, hex
56 56 from mercurial.encoding import tolocal
57 57 from mercurial.discovery import findcommonoutgoing
58 58 from mercurial.hg import peer
59 59 from mercurial.httppeer import makepeer
60 60 from mercurial.utils.urlutil import url as hg_url
61 61 from mercurial.scmutil import revrange, revsymbol
62 62 from mercurial.node import nullrev
63 63 from mercurial import exchange
64 64 from hgext import largefiles
65 65
66 66 # those authnadlers are patched for python 2.6.5 bug an
67 67 # infinit looping when given invalid resources
68 68 from mercurial.url import httpbasicauthhandler, httpdigestauthhandler
69 69
70 70 # hg strip is in core now
71 71 from mercurial import strip as hgext_strip
72 72
73 73
74 74 def get_ctx(repo, ref):
75 ref = safe_bytes(ref)
75 if not isinstance(ref, int):
76 ref = safe_bytes(ref)
76 77 try:
77 78 ctx = repo[ref]
78 79 except (ProgrammingError, TypeError):
79 80 # we're unable to find the rev using a regular lookup, we fallback
80 81 # to slower, but backward compat revsymbol usage
81 82 ctx = revsymbol(repo, ref)
82 83 except (LookupError, RepoLookupError):
83 84 # Similar case as above but only for refs that are not numeric
84 85 if isinstance(ref, int):
85 86 raise
86 87 ctx = revsymbol(repo, ref)
87 88 return ctx
@@ -1,770 +1,773 b''
1 1 # RhodeCode VCSServer provides access to different vcs backends via network.
2 2 # Copyright (C) 2014-2020 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 io
19 19 import os
20 20 import sys
21 21 import base64
22 22 import locale
23 23 import logging
24 24 import uuid
25 25 import time
26 26 import wsgiref.util
27 27 import traceback
28 28 import tempfile
29 29 import psutil
30 30
31 31 from itertools import chain
32 32
33 33 import msgpack
34 34 import configparser
35 35
36 36 from pyramid.config import Configurator
37 37 from pyramid.wsgi import wsgiapp
38 38 from pyramid.response import Response
39 39
40 40 from vcsserver.base import BinaryEnvelope
41 41 from vcsserver.lib.rc_json import json
42 42 from vcsserver.config.settings_maker import SettingsMaker
43 43 from vcsserver.str_utils import safe_int, safe_bytes, safe_str
44 44 from vcsserver.lib.statsd_client import StatsdClient
45 45
46 46 log = logging.getLogger(__name__)
47 47
48 48 # due to Mercurial/glibc2.27 problems we need to detect if locale settings are
49 49 # causing problems and "fix" it in case they do and fallback to LC_ALL = C
50 50
51 51 try:
52 52 locale.setlocale(locale.LC_ALL, '')
53 53 except locale.Error as e:
54 54 log.error(
55 55 'LOCALE ERROR: failed to set LC_ALL, fallback to LC_ALL=C, org error: %s', e)
56 56 os.environ['LC_ALL'] = 'C'
57 57
58 58
59 59 import vcsserver
60 60 from vcsserver import remote_wsgi, scm_app, settings, hgpatches
61 61 from vcsserver.git_lfs.app import GIT_LFS_CONTENT_TYPE, GIT_LFS_PROTO_PAT
62 62 from vcsserver.echo_stub import remote_wsgi as remote_wsgi_stub
63 63 from vcsserver.echo_stub.echo_app import EchoApp
64 64 from vcsserver.exceptions import HTTPRepoLocked, HTTPRepoBranchProtected
65 65 from vcsserver.lib.exc_tracking import store_exception
66 66 from vcsserver.server import VcsServer
67 67
68 68 strict_vcs = True
69 69
70 70 git_import_err = None
71 71 try:
72 72 from vcsserver.remote.git import GitFactory, GitRemote
73 73 except ImportError as e:
74 74 GitFactory = None
75 75 GitRemote = None
76 76 git_import_err = e
77 77 if strict_vcs:
78 78 raise
79 79
80 80
81 81 hg_import_err = None
82 82 try:
83 83 from vcsserver.remote.hg import MercurialFactory, HgRemote
84 84 except ImportError as e:
85 85 MercurialFactory = None
86 86 HgRemote = None
87 87 hg_import_err = e
88 88 if strict_vcs:
89 89 raise
90 90
91 91
92 92 svn_import_err = None
93 93 try:
94 94 from vcsserver.remote.svn import SubversionFactory, SvnRemote
95 95 except ImportError as e:
96 96 SubversionFactory = None
97 97 SvnRemote = None
98 98 svn_import_err = e
99 99 if strict_vcs:
100 100 raise
101 101
102 102
103 103 def _is_request_chunked(environ):
104 104 stream = environ.get('HTTP_TRANSFER_ENCODING', '') == 'chunked'
105 105 return stream
106 106
107 107
108 108 def log_max_fd():
109 109 try:
110 110 maxfd = psutil.Process().rlimit(psutil.RLIMIT_NOFILE)[1]
111 111 log.info('Max file descriptors value: %s', maxfd)
112 112 except Exception:
113 113 pass
114 114
115 115
116 116 class VCS(object):
117 117 def __init__(self, locale_conf=None, cache_config=None):
118 118 self.locale = locale_conf
119 119 self.cache_config = cache_config
120 120 self._configure_locale()
121 121
122 122 log_max_fd()
123 123
124 124 if GitFactory and GitRemote:
125 125 git_factory = GitFactory()
126 126 self._git_remote = GitRemote(git_factory)
127 127 else:
128 128 log.error("Git client import failed: %s", git_import_err)
129 129
130 130 if MercurialFactory and HgRemote:
131 131 hg_factory = MercurialFactory()
132 132 self._hg_remote = HgRemote(hg_factory)
133 133 else:
134 134 log.error("Mercurial client import failed: %s", hg_import_err)
135 135
136 136 if SubversionFactory and SvnRemote:
137 137 svn_factory = SubversionFactory()
138 138
139 139 # hg factory is used for svn url validation
140 140 hg_factory = MercurialFactory()
141 141 self._svn_remote = SvnRemote(svn_factory, hg_factory=hg_factory)
142 142 else:
143 143 log.error("Subversion client import failed: %s", svn_import_err)
144 144
145 145 self._vcsserver = VcsServer()
146 146
147 147 def _configure_locale(self):
148 148 if self.locale:
149 149 log.info('Settings locale: `LC_ALL` to %s', self.locale)
150 150 else:
151 151 log.info('Configuring locale subsystem based on environment variables')
152 152 try:
153 153 # If self.locale is the empty string, then the locale
154 154 # module will use the environment variables. See the
155 155 # documentation of the package `locale`.
156 156 locale.setlocale(locale.LC_ALL, self.locale)
157 157
158 158 language_code, encoding = locale.getlocale()
159 159 log.info(
160 160 'Locale set to language code "%s" with encoding "%s".',
161 161 language_code, encoding)
162 162 except locale.Error:
163 163 log.exception('Cannot set locale, not configuring the locale system')
164 164
165 165
166 166 class WsgiProxy(object):
167 167 def __init__(self, wsgi):
168 168 self.wsgi = wsgi
169 169
170 170 def __call__(self, environ, start_response):
171 171 input_data = environ['wsgi.input'].read()
172 172 input_data = msgpack.unpackb(input_data)
173 173
174 174 error = None
175 175 try:
176 176 data, status, headers = self.wsgi.handle(
177 177 input_data['environment'], input_data['input_data'],
178 178 *input_data['args'], **input_data['kwargs'])
179 179 except Exception as e:
180 180 data, status, headers = [], None, None
181 181 error = {
182 182 'message': str(e),
183 183 '_vcs_kind': getattr(e, '_vcs_kind', None)
184 184 }
185 185
186 186 start_response(200, {})
187 187 return self._iterator(error, status, headers, data)
188 188
189 189 def _iterator(self, error, status, headers, data):
190 190 initial_data = [
191 191 error,
192 192 status,
193 193 headers,
194 194 ]
195 195
196 196 for d in chain(initial_data, data):
197 197 yield msgpack.packb(d)
198 198
199 199
200 200 def not_found(request):
201 201 return {'status': '404 NOT FOUND'}
202 202
203 203
204 204 class VCSViewPredicate(object):
205 205 def __init__(self, val, config):
206 206 self.remotes = val
207 207
208 208 def text(self):
209 209 return 'vcs view method = %s' % (list(self.remotes.keys()),)
210 210
211 211 phash = text
212 212
213 213 def __call__(self, context, request):
214 214 """
215 215 View predicate that returns true if given backend is supported by
216 216 defined remotes.
217 217 """
218 218 backend = request.matchdict.get('backend')
219 219 return backend in self.remotes
220 220
221 221
222 222 class HTTPApplication(object):
223 223 ALLOWED_EXCEPTIONS = ('KeyError', 'URLError')
224 224
225 225 remote_wsgi = remote_wsgi
226 226 _use_echo_app = False
227 227
228 228 def __init__(self, settings=None, global_config=None):
229 229
230 230 self.config = Configurator(settings=settings)
231 231 # Init our statsd at very start
232 232 self.config.registry.statsd = StatsdClient.statsd
233 233 self.config.registry.vcs_call_context = {}
234 234
235 235 self.global_config = global_config
236 236 self.config.include('vcsserver.lib.rc_cache')
237 237
238 238 settings_locale = settings.get('locale', '') or 'en_US.UTF-8'
239 239 vcs = VCS(locale_conf=settings_locale, cache_config=settings)
240 240 self._remotes = {
241 241 'hg': vcs._hg_remote,
242 242 'git': vcs._git_remote,
243 243 'svn': vcs._svn_remote,
244 244 'server': vcs._vcsserver,
245 245 }
246 246 if settings.get('dev.use_echo_app', 'false').lower() == 'true':
247 247 self._use_echo_app = True
248 248 log.warning("Using EchoApp for VCS operations.")
249 249 self.remote_wsgi = remote_wsgi_stub
250 250
251 251 self._configure_settings(global_config, settings)
252 252
253 253 self._configure()
254 254
255 255 def _configure_settings(self, global_config, app_settings):
256 256 """
257 257 Configure the settings module.
258 258 """
259 259 settings_merged = global_config.copy()
260 260 settings_merged.update(app_settings)
261 261
262 262 git_path = app_settings.get('git_path', None)
263 263 if git_path:
264 264 settings.GIT_EXECUTABLE = git_path
265 265 binary_dir = app_settings.get('core.binary_dir', None)
266 266 if binary_dir:
267 267 settings.BINARY_DIR = binary_dir
268 268
269 269 # Store the settings to make them available to other modules.
270 270 vcsserver.PYRAMID_SETTINGS = settings_merged
271 271 vcsserver.CONFIG = settings_merged
272 272
273 273 def _configure(self):
274 274 self.config.add_renderer(name='msgpack', factory=self._msgpack_renderer_factory)
275 275
276 276 self.config.add_route('service', '/_service')
277 277 self.config.add_route('status', '/status')
278 278 self.config.add_route('hg_proxy', '/proxy/hg')
279 279 self.config.add_route('git_proxy', '/proxy/git')
280 280
281 281 # rpc methods
282 282 self.config.add_route('vcs', '/{backend}')
283 283
284 284 # streaming rpc remote methods
285 285 self.config.add_route('vcs_stream', '/{backend}/stream')
286 286
287 287 # vcs operations clone/push as streaming
288 288 self.config.add_route('stream_git', '/stream/git/*repo_name')
289 289 self.config.add_route('stream_hg', '/stream/hg/*repo_name')
290 290
291 291 self.config.add_view(self.status_view, route_name='status', renderer='json')
292 292 self.config.add_view(self.service_view, route_name='service', renderer='msgpack')
293 293
294 294 self.config.add_view(self.hg_proxy(), route_name='hg_proxy')
295 295 self.config.add_view(self.git_proxy(), route_name='git_proxy')
296 296 self.config.add_view(self.vcs_view, route_name='vcs', renderer='msgpack',
297 297 vcs_view=self._remotes)
298 298 self.config.add_view(self.vcs_stream_view, route_name='vcs_stream',
299 299 vcs_view=self._remotes)
300 300
301 301 self.config.add_view(self.hg_stream(), route_name='stream_hg')
302 302 self.config.add_view(self.git_stream(), route_name='stream_git')
303 303
304 304 self.config.add_view_predicate('vcs_view', VCSViewPredicate)
305 305
306 306 self.config.add_notfound_view(not_found, renderer='json')
307 307
308 308 self.config.add_view(self.handle_vcs_exception, context=Exception)
309 309
310 310 self.config.add_tween(
311 311 'vcsserver.tweens.request_wrapper.RequestWrapperTween',
312 312 )
313 313 self.config.add_request_method(
314 314 'vcsserver.lib.request_counter.get_request_counter',
315 315 'request_count')
316 316
317 317 def wsgi_app(self):
318 318 return self.config.make_wsgi_app()
319 319
320 320 def _vcs_view_params(self, request):
321 321 remote = self._remotes[request.matchdict['backend']]
322 322 payload = msgpack.unpackb(request.body, use_list=True)
323 323
324 324 method = payload.get('method')
325 325 params = payload['params']
326 326 wire = params.get('wire')
327 327 args = params.get('args')
328 328 kwargs = params.get('kwargs')
329 329 context_uid = None
330 330
331 331 request.registry.vcs_call_context = {
332 332 'method': method,
333 333 'repo_name': payload.get('_repo_name')
334 334 }
335 335
336 336 if wire:
337 337 try:
338 338 wire['context'] = context_uid = uuid.UUID(wire['context'])
339 339 except KeyError:
340 340 pass
341 341 args.insert(0, wire)
342 342 repo_state_uid = wire.get('repo_state_uid') if wire else None
343 343
344 344 # NOTE(marcink): trading complexity for slight performance
345 345 if log.isEnabledFor(logging.DEBUG):
346 346 # also we SKIP printing out any of those methods args since they maybe excessive
347 347 just_args_methods = {
348 348 'commitctx': ('content', 'removed', 'updated')
349 349 }
350 350 if method in just_args_methods:
351 351 skip_args = just_args_methods[method]
352 352 call_args = ''
353 353 call_kwargs = {}
354 354 for k in kwargs:
355 355 if k in skip_args:
356 356 # replace our skip key with dummy
357 357 call_kwargs[k] = f'RemovedParam({k})'
358 358 else:
359 359 call_kwargs[k] = kwargs[k]
360 360 else:
361 361 call_args = args[1:]
362 362 call_kwargs = kwargs
363 363
364 364 log.debug('Method requested:`%s` with args:%s kwargs:%s context_uid: %s, repo_state_uid:%s',
365 365 method, call_args, call_kwargs, context_uid, repo_state_uid)
366 366
367 367 statsd = request.registry.statsd
368 368 if statsd:
369 369 statsd.incr(
370 370 'vcsserver_method_total', tags=[
371 371 "method:{}".format(method),
372 372 ])
373 373 return payload, remote, method, args, kwargs
374 374
375 375 def vcs_view(self, request):
376 376
377 377 payload, remote, method, args, kwargs = self._vcs_view_params(request)
378 378 payload_id = payload.get('id')
379 379
380 380 try:
381 381 resp = getattr(remote, method)(*args, **kwargs)
382 382 except Exception as e:
383 383 exc_info = list(sys.exc_info())
384 384 exc_type, exc_value, exc_traceback = exc_info
385 385
386 386 org_exc = getattr(e, '_org_exc', None)
387 387 org_exc_name = None
388 388 org_exc_tb = ''
389 389 if org_exc:
390 390 org_exc_name = org_exc.__class__.__name__
391 391 org_exc_tb = getattr(e, '_org_exc_tb', '')
392 392 # replace our "faked" exception with our org
393 393 exc_info[0] = org_exc.__class__
394 394 exc_info[1] = org_exc
395 395
396 396 should_store_exc = True
397 397 if org_exc:
398 398 def get_exc_fqn(_exc_obj):
399 399 module_name = getattr(org_exc.__class__, '__module__', 'UNKNOWN')
400 400 return module_name + '.' + org_exc_name
401 401
402 402 exc_fqn = get_exc_fqn(org_exc)
403 403
404 404 if exc_fqn in ['mercurial.error.RepoLookupError',
405 405 'vcsserver.exceptions.RefNotFoundException']:
406 406 should_store_exc = False
407 407
408 408 if should_store_exc:
409 409 store_exception(id(exc_info), exc_info, request_path=request.path)
410 410
411 411 tb_info = ''.join(
412 412 traceback.format_exception(exc_type, exc_value, exc_traceback))
413 413
414 414 type_ = e.__class__.__name__
415 415 if type_ not in self.ALLOWED_EXCEPTIONS:
416 416 type_ = None
417 417
418 418 resp = {
419 419 'id': payload_id,
420 420 'error': {
421 421 'message': str(e),
422 422 'traceback': tb_info,
423 423 'org_exc': org_exc_name,
424 424 'org_exc_tb': org_exc_tb,
425 425 'type': type_
426 426 }
427 427 }
428 428
429 429 try:
430 430 resp['error']['_vcs_kind'] = getattr(e, '_vcs_kind', None)
431 431 except AttributeError:
432 432 pass
433 433 else:
434 434 resp = {
435 435 'id': payload_id,
436 436 'result': resp
437 437 }
438 438 log.debug('Serving data for method %s', method)
439 439 return resp
440 440
441 441 def vcs_stream_view(self, request):
442 442 payload, remote, method, args, kwargs = self._vcs_view_params(request)
443 443 # this method has a stream: marker we remove it here
444 444 method = method.split('stream:')[-1]
445 445 chunk_size = safe_int(payload.get('chunk_size')) or 4096
446 446
447 447 try:
448 448 resp = getattr(remote, method)(*args, **kwargs)
449 449 except Exception as e:
450 450 raise
451 451
452 452 def get_chunked_data(method_resp):
453 453 stream = io.BytesIO(method_resp)
454 454 while 1:
455 455 chunk = stream.read(chunk_size)
456 456 if not chunk:
457 457 break
458 458 yield chunk
459 459
460 460 response = Response(app_iter=get_chunked_data(resp))
461 461 response.content_type = 'application/octet-stream'
462 462
463 463 return response
464 464
465 465 def status_view(self, request):
466 466 import vcsserver
467 467 return {'status': 'OK', 'vcsserver_version': vcsserver.__version__,
468 468 'pid': os.getpid()}
469 469
470 470 def service_view(self, request):
471 471 import vcsserver
472 472
473 473 payload = msgpack.unpackb(request.body, use_list=True)
474 474 server_config, app_config = {}, {}
475 475
476 476 try:
477 477 path = self.global_config['__file__']
478 478 config = configparser.RawConfigParser()
479 479
480 480 config.read(path)
481 481
482 482 if config.has_section('server:main'):
483 483 server_config = dict(config.items('server:main'))
484 484 if config.has_section('app:main'):
485 485 app_config = dict(config.items('app:main'))
486 486
487 487 except Exception:
488 488 log.exception('Failed to read .ini file for display')
489 489
490 490 environ = list(os.environ.items())
491 491
492 492 resp = {
493 493 'id': payload.get('id'),
494 494 'result': dict(
495 495 version=vcsserver.__version__,
496 496 config=server_config,
497 497 app_config=app_config,
498 498 environ=environ,
499 499 payload=payload,
500 500 )
501 501 }
502 502 return resp
503 503
504 504 def _msgpack_renderer_factory(self, info):
505 505
506 506 def _render(value, system):
507 507 bin_type = False
508 508 res = value.get('result')
509 509 if res and isinstance(res, BinaryEnvelope):
510 log.debug('Result is wrapped in BinaryEnvelope type')
510 511 value['result'] = res.value
511 512 bin_type = res.bin_type
512 513
513 514 request = system.get('request')
514 515 if request is not None:
515 516 response = request.response
516 517 ct = response.content_type
517 518 if ct == response.default_content_type:
518 519 response.content_type = 'application/x-msgpack'
520 if bin_type:
521 response.content_type = 'application/x-msgpack-bin'
519 522
520 523 return msgpack.packb(value, use_bin_type=bin_type)
521 524 return _render
522 525
523 526 def set_env_from_config(self, environ, config):
524 527 dict_conf = {}
525 528 try:
526 529 for elem in config:
527 530 if elem[0] == 'rhodecode':
528 531 dict_conf = json.loads(elem[2])
529 532 break
530 533 except Exception:
531 534 log.exception('Failed to fetch SCM CONFIG')
532 535 return
533 536
534 537 username = dict_conf.get('username')
535 538 if username:
536 539 environ['REMOTE_USER'] = username
537 540 # mercurial specific, some extension api rely on this
538 541 environ['HGUSER'] = username
539 542
540 543 ip = dict_conf.get('ip')
541 544 if ip:
542 545 environ['REMOTE_HOST'] = ip
543 546
544 547 if _is_request_chunked(environ):
545 548 # set the compatibility flag for webob
546 549 environ['wsgi.input_terminated'] = True
547 550
548 551 def hg_proxy(self):
549 552 @wsgiapp
550 553 def _hg_proxy(environ, start_response):
551 554 app = WsgiProxy(self.remote_wsgi.HgRemoteWsgi())
552 555 return app(environ, start_response)
553 556 return _hg_proxy
554 557
555 558 def git_proxy(self):
556 559 @wsgiapp
557 560 def _git_proxy(environ, start_response):
558 561 app = WsgiProxy(self.remote_wsgi.GitRemoteWsgi())
559 562 return app(environ, start_response)
560 563 return _git_proxy
561 564
562 565 def hg_stream(self):
563 566 if self._use_echo_app:
564 567 @wsgiapp
565 568 def _hg_stream(environ, start_response):
566 569 app = EchoApp('fake_path', 'fake_name', None)
567 570 return app(environ, start_response)
568 571 return _hg_stream
569 572 else:
570 573 @wsgiapp
571 574 def _hg_stream(environ, start_response):
572 575 log.debug('http-app: handling hg stream')
573 576
574 577 packed_cc = base64.b64decode(environ['HTTP_X_RC_VCS_STREAM_CALL_CONTEXT'])
575 578 call_context = msgpack.unpackb(packed_cc)
576 579
577 580 repo_path = call_context['repo_path']
578 581 repo_name = call_context['repo_name']
579 582 config = call_context['repo_config']
580 583
581 584 app = scm_app.create_hg_wsgi_app(
582 585 repo_path, repo_name, config)
583 586
584 587 # Consistent path information for hgweb
585 588 environ['PATH_INFO'] = call_context['path_info']
586 589 environ['REPO_NAME'] = repo_name
587 590 self.set_env_from_config(environ, config)
588 591
589 592 log.debug('http-app: starting app handler '
590 593 'with %s and process request', app)
591 594 return app(environ, ResponseFilter(start_response))
592 595 return _hg_stream
593 596
594 597 def git_stream(self):
595 598 if self._use_echo_app:
596 599 @wsgiapp
597 600 def _git_stream(environ, start_response):
598 601 app = EchoApp('fake_path', 'fake_name', None)
599 602 return app(environ, start_response)
600 603 return _git_stream
601 604 else:
602 605 @wsgiapp
603 606 def _git_stream(environ, start_response):
604 607 log.debug('http-app: handling git stream')
605 608
606 609 packed_cc = base64.b64decode(environ['HTTP_X_RC_VCS_STREAM_CALL_CONTEXT'])
607 610 call_context = msgpack.unpackb(packed_cc)
608 611
609 612 repo_path = call_context['repo_path']
610 613 repo_name = call_context['repo_name']
611 614 config = call_context['repo_config']
612 615
613 616 environ['PATH_INFO'] = call_context['path_info']
614 617 self.set_env_from_config(environ, config)
615 618
616 619 content_type = environ.get('CONTENT_TYPE', '')
617 620
618 621 path = environ['PATH_INFO']
619 622 is_lfs_request = GIT_LFS_CONTENT_TYPE in content_type
620 623 log.debug(
621 624 'LFS: Detecting if request `%s` is LFS server path based '
622 625 'on content type:`%s`, is_lfs:%s',
623 626 path, content_type, is_lfs_request)
624 627
625 628 if not is_lfs_request:
626 629 # fallback detection by path
627 630 if GIT_LFS_PROTO_PAT.match(path):
628 631 is_lfs_request = True
629 632 log.debug(
630 633 'LFS: fallback detection by path of: `%s`, is_lfs:%s',
631 634 path, is_lfs_request)
632 635
633 636 if is_lfs_request:
634 637 app = scm_app.create_git_lfs_wsgi_app(
635 638 repo_path, repo_name, config)
636 639 else:
637 640 app = scm_app.create_git_wsgi_app(
638 641 repo_path, repo_name, config)
639 642
640 643 log.debug('http-app: starting app handler '
641 644 'with %s and process request', app)
642 645
643 646 return app(environ, start_response)
644 647
645 648 return _git_stream
646 649
647 650 def handle_vcs_exception(self, exception, request):
648 651 _vcs_kind = getattr(exception, '_vcs_kind', '')
649 652 if _vcs_kind == 'repo_locked':
650 653 # Get custom repo-locked status code if present.
651 654 status_code = request.headers.get('X-RC-Locked-Status-Code')
652 655 return HTTPRepoLocked(
653 656 title=exception.message, status_code=status_code)
654 657
655 658 elif _vcs_kind == 'repo_branch_protected':
656 659 # Get custom repo-branch-protected status code if present.
657 660 return HTTPRepoBranchProtected(title=exception.message)
658 661
659 662 exc_info = request.exc_info
660 663 store_exception(id(exc_info), exc_info)
661 664
662 665 traceback_info = 'unavailable'
663 666 if request.exc_info:
664 667 exc_type, exc_value, exc_tb = request.exc_info
665 668 traceback_info = ''.join(traceback.format_exception(exc_type, exc_value, exc_tb))
666 669
667 670 log.error(
668 671 'error occurred handling this request for path: %s, \n tb: %s',
669 672 request.path, traceback_info)
670 673
671 674 statsd = request.registry.statsd
672 675 if statsd:
673 676 exc_type = "{}.{}".format(exception.__class__.__module__, exception.__class__.__name__)
674 677 statsd.incr('vcsserver_exception_total',
675 678 tags=["type:{}".format(exc_type)])
676 679 raise exception
677 680
678 681
679 682 class ResponseFilter(object):
680 683
681 684 def __init__(self, start_response):
682 685 self._start_response = start_response
683 686
684 687 def __call__(self, status, response_headers, exc_info=None):
685 688 headers = tuple(
686 689 (h, v) for h, v in response_headers
687 690 if not wsgiref.util.is_hop_by_hop(h))
688 691 return self._start_response(status, headers, exc_info)
689 692
690 693
691 694 def sanitize_settings_and_apply_defaults(global_config, settings):
692 695 global_settings_maker = SettingsMaker(global_config)
693 696 settings_maker = SettingsMaker(settings)
694 697
695 698 settings_maker.make_setting('logging.autoconfigure', False, parser='bool')
696 699
697 700 logging_conf = os.path.join(os.path.dirname(global_config.get('__file__')), 'logging.ini')
698 701 settings_maker.enable_logging(logging_conf)
699 702
700 703 # Default includes, possible to change as a user
701 704 pyramid_includes = settings_maker.make_setting('pyramid.includes', [], parser='list:newline')
702 705 log.debug("Using the following pyramid.includes: %s", pyramid_includes)
703 706
704 707 settings_maker.make_setting('__file__', global_config.get('__file__'))
705 708
706 709 settings_maker.make_setting('pyramid.default_locale_name', 'en')
707 710 settings_maker.make_setting('locale', 'en_US.UTF-8')
708 711
709 712 settings_maker.make_setting('core.binary_dir', '')
710 713
711 714 temp_store = tempfile.gettempdir()
712 715 default_cache_dir = os.path.join(temp_store, 'rc_cache')
713 716 # save default, cache dir, and use it for all backends later.
714 717 default_cache_dir = settings_maker.make_setting(
715 718 'cache_dir',
716 719 default=default_cache_dir, default_when_empty=True,
717 720 parser='dir:ensured')
718 721
719 722 # exception store cache
720 723 settings_maker.make_setting(
721 724 'exception_tracker.store_path',
722 725 default=os.path.join(default_cache_dir, 'exc_store'), default_when_empty=True,
723 726 parser='dir:ensured'
724 727 )
725 728
726 729 # repo_object cache defaults
727 730 settings_maker.make_setting(
728 731 'rc_cache.repo_object.backend',
729 732 default='dogpile.cache.rc.file_namespace',
730 733 parser='string')
731 734 settings_maker.make_setting(
732 735 'rc_cache.repo_object.expiration_time',
733 736 default=30 * 24 * 60 * 60, # 30days
734 737 parser='int')
735 738 settings_maker.make_setting(
736 739 'rc_cache.repo_object.arguments.filename',
737 740 default=os.path.join(default_cache_dir, 'vcsserver_cache_repo_object.db'),
738 741 parser='string')
739 742
740 743 # statsd
741 744 settings_maker.make_setting('statsd.enabled', False, parser='bool')
742 745 settings_maker.make_setting('statsd.statsd_host', 'statsd-exporter', parser='string')
743 746 settings_maker.make_setting('statsd.statsd_port', 9125, parser='int')
744 747 settings_maker.make_setting('statsd.statsd_prefix', '')
745 748 settings_maker.make_setting('statsd.statsd_ipv6', False, parser='bool')
746 749
747 750 settings_maker.env_expand()
748 751
749 752
750 753 def main(global_config, **settings):
751 754 start_time = time.time()
752 755 log.info('Pyramid app config starting')
753 756
754 757 if MercurialFactory:
755 758 hgpatches.patch_largefiles_capabilities()
756 759 hgpatches.patch_subrepo_type_mapping()
757 760
758 761 # Fill in and sanitize the defaults & do ENV expansion
759 762 sanitize_settings_and_apply_defaults(global_config, settings)
760 763
761 764 # init and bootstrap StatsdClient
762 765 StatsdClient.setup(settings)
763 766
764 767 pyramid_app = HTTPApplication(settings=settings, global_config=global_config).wsgi_app()
765 768 total_time = time.time() - start_time
766 769 log.info('Pyramid app `%s` created and configured in %.2fs',
767 770 getattr(pyramid_app, 'func_name', 'pyramid_app'), total_time)
768 771 return pyramid_app
769 772
770 773
General Comments 0
You need to be logged in to leave comments. Login now