##// END OF EJS Templates
middlewares: all porting for python3
super-admin -
r5082:35aadafc default
parent child Browse files
Show More
@@ -1,100 +1,105 b''
1 1
2 2 # Copyright (C) 2010-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 Affero General Public License, version 3
6 6 # (only), as published by the Free Software Foundation.
7 7 #
8 8 # This program is distributed in the hope that it will be useful,
9 9 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 11 # GNU General Public License for more details.
12 12 #
13 13 # You should have received a copy of the GNU Affero General Public License
14 14 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 15 #
16 16 # This program is dual-licensed. If you wish to learn more about the
17 17 # RhodeCode Enterprise Edition, including its added features, Support services,
18 18 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 19
20 20 """
21 21 middleware to handle appenlight publishing of errors
22 22 """
23 23 import logging
24 24
25 25 log = logging.getLogger(__name__)
26 26
27 try:
28 from appenlight_client.exceptions import get_current_traceback
29 except ImportError:
30 def get_current_traceback(*args, **kwargs):
31 return
32
27 33
28 34 def track_exception(environ):
29 from appenlight_client.exceptions import get_current_traceback
30 35
31 36 if 'appenlight.client' not in environ:
32 37 return
33 38
34 39 # pass the traceback object to middleware
35 40 environ['appenlight.__traceback'] = get_current_traceback(
36 41 skip=1,
37 42 show_hidden_frames=True,
38 43 ignore_system_exceptions=True
39 44 )
40 45
41 46
42 47 def track_extra_information(environ, section, value):
43 48 """
44 49 Utility function to attach extra information in case of an error condition.
45 50
46 51 It will take care of attaching this information to the right place inside
47 52 of `environ`, so that the appenight client can pick it up.
48 53 """
49 54 environ.setdefault('appenlight.extra', {})
50 55 environ['appenlight.extra'][section] = value
51 56
52 57
53 58 def wrap_in_appenlight_if_enabled(app, settings, appenlight_client=None):
54 59 """
55 60 Wraps the given `app` for appenlight support.
56 61
57 62 .. important::
58 63
59 64 Appenlight expects that the wrapper is executed only once, that's why
60 65 the parameter `appenlight_client` can be used to pass in an already
61 66 existing client instance to avoid that decorators are applied more than
62 67 once.
63 68
64 69 This is in use to support our setup of the vcs related middlewares.
65 70
66 71 """
67 72 if settings['appenlight']:
68 73 try:
69 74 from appenlight_client import make_appenlight_middleware
70 75 from appenlight_client.wsgi import AppenlightWSGIWrapper
71 76 except ImportError:
72 77 log.info('Appenlight packages not present, skipping appenlight setup')
73 78 return app, appenlight_client
74 79
75 80 app = RemoteTracebackTracker(app)
76 81 if not appenlight_client:
77 82 app = make_appenlight_middleware(app, settings)
78 83 appenlight_client = app.appenlight_client
79 84 else:
80 85 app = AppenlightWSGIWrapper(app, appenlight_client)
81 86
82 87 return app, appenlight_client
83 88
84 89
85 90 class RemoteTracebackTracker(object):
86 91 """
87 92 Utility middleware which forwards VCSServer remote traceback information.
88 93 """
89 94
90 95 def __init__(self, app):
91 96 self.application = app
92 97
93 98 def __call__(self, environ, start_response):
94 99 try:
95 100 return self.application(environ, start_response)
96 101 except Exception as e:
97 102 if hasattr(e, '_vcs_server_traceback'):
98 103 track_extra_information(
99 104 environ, 'remote_traceback', e._vcs_server_traceback)
100 105 raise
@@ -1,94 +1,96 b''
1 1
2 2
3 3 # Copyright (C) 2016-2020 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
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 Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 import time
22 22 import logging
23 23
24 24 import rhodecode
25 25 from rhodecode.lib.auth import AuthUser
26 26 from rhodecode.lib.base import get_ip_addr, get_user_agent
27 27 from rhodecode.lib.middleware.utils import get_path_info
28 28 from rhodecode.lib.utils2 import safe_str, get_current_rhodecode_user
29 29
30 30
31 31 log = logging.getLogger(__name__)
32 32
33 33
34 34 class RequestWrapperTween(object):
35 35 def __init__(self, handler, registry):
36 36 self.handler = handler
37 37 self.registry = registry
38 38
39 39 # one-time configuration code goes here
40 40
41 41 def _get_user_info(self, request):
42 42 user = get_current_rhodecode_user(request)
43 43 if not user:
44 44 user = AuthUser.repr_user(ip=get_ip_addr(request.environ))
45 45 return user
46 46
47 47 def __call__(self, request):
48 48 start = time.time()
49 49 log.debug('Starting request time measurement')
50 50 response = None
51 request.req_wrapper_start = start
52
51 53 try:
52 54 response = self.handler(request)
53 55 finally:
54 56 count = request.request_count()
55 57 _ver_ = rhodecode.__version__
56 58 _path = get_path_info(request.environ)
57 59 _auth_user = self._get_user_info(request)
58 60 ip = get_ip_addr(request.environ)
59 61 match_route = request.matched_route.name if request.matched_route else "NOT_FOUND"
60 62 resp_code = getattr(response, 'status_code', 'UNDEFINED')
61 63
62 64 total = time.time() - start
63 65 log.info(
64 66 'Req[%4s] %s %s Request to %s time: %.4fs [%s], RhodeCode %s',
65 67 count, _auth_user, request.environ.get('REQUEST_METHOD'),
66 68 _path, total, get_user_agent(request. environ), _ver_,
67 69 extra={"time": total, "ver": _ver_, "ip": ip,
68 70 "path": _path, "view_name": match_route, "code": resp_code}
69 71 )
70 72
71 73 statsd = request.registry.statsd
72 74 if statsd:
73 75 elapsed_time_ms = round(1000.0 * total) # use ms only
74 76 statsd.timing(
75 77 "rhodecode_req_timing.histogram", elapsed_time_ms,
76 78 tags=[
77 79 "view_name:{}".format(match_route),
78 80 "code:{}".format(resp_code)
79 81 ],
80 82 use_decimals=False
81 83 )
82 84 statsd.incr(
83 85 'rhodecode_req_total', tags=[
84 86 "view_name:{}".format(match_route),
85 87 "code:{}".format(resp_code)
86 88 ])
87 89
88 90 return response
89 91
90 92
91 93 def includeme(config):
92 94 config.add_tween(
93 95 'rhodecode.lib.middleware.request_wrapper.RequestWrapperTween',
94 96 )
@@ -1,159 +1,161 b''
1 1
2 2 # Copyright (C) 2010-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 Affero General Public License, version 3
6 6 # (only), as published by the Free Software Foundation.
7 7 #
8 8 # This program is distributed in the hope that it will be useful,
9 9 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 11 # GNU General Public License for more details.
12 12 #
13 13 # You should have received a copy of the GNU Affero General Public License
14 14 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 15 #
16 16 # This program is dual-licensed. If you wish to learn more about the
17 17 # RhodeCode Enterprise Edition, including its added features, Support services,
18 18 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 19
20 20 """
21 21 SimpleHG middleware for handling mercurial protocol request
22 22 (push/clone etc.). It's implemented with basic auth function
23 23 """
24 24
25 25 import logging
26 26 import urllib.parse
27 import urllib.request, urllib.parse, urllib.error
27 import urllib.request
28 import urllib.parse
29 import urllib.error
28 30
29 31 from rhodecode.lib import utils
30 32 from rhodecode.lib.ext_json import json
31 33 from rhodecode.lib.middleware import simplevcs
32 34 from rhodecode.lib.middleware.utils import get_path_info
33 35
34 36 log = logging.getLogger(__name__)
35 37
36 38
37 39 class SimpleHg(simplevcs.SimpleVCS):
38 40
39 41 SCM = 'hg'
40 42
41 43 def _get_repository_name(self, environ):
42 44 """
43 45 Gets repository name out of PATH_INFO header
44 46
45 47 :param environ: environ where PATH_INFO is stored
46 48 """
47 49 repo_name = get_path_info(environ)
48 50 if repo_name and repo_name.startswith('/'):
49 51 # remove only the first leading /
50 52 repo_name = repo_name[1:]
51 53 return repo_name.rstrip('/')
52 54
53 55 _ACTION_MAPPING = {
54 56 'changegroup': 'pull',
55 57 'changegroupsubset': 'pull',
56 58 'getbundle': 'pull',
57 59 'stream_out': 'pull',
58 60 'listkeys': 'pull',
59 61 'between': 'pull',
60 62 'branchmap': 'pull',
61 63 'branches': 'pull',
62 64 'clonebundles': 'pull',
63 65 'capabilities': 'pull',
64 66 'debugwireargs': 'pull',
65 67 'heads': 'pull',
66 68 'lookup': 'pull',
67 69 'hello': 'pull',
68 70 'known': 'pull',
69 71
70 72 # largefiles
71 73 'putlfile': 'push',
72 74 'getlfile': 'pull',
73 75 'statlfile': 'pull',
74 76 'lheads': 'pull',
75 77
76 78 # evolve
77 79 'evoext_obshashrange_v1': 'pull',
78 80 'evoext_obshash': 'pull',
79 81 'evoext_obshash1': 'pull',
80 82
81 83 'unbundle': 'push',
82 84 'pushkey': 'push',
83 85 }
84 86
85 87 @classmethod
86 88 def _get_xarg_headers(cls, environ):
87 89 i = 1
88 90 chunks = [] # gather chunks stored in multiple 'hgarg_N'
89 91 while True:
90 92 head = environ.get('HTTP_X_HGARG_{}'.format(i))
91 93 if not head:
92 94 break
93 95 i += 1
94 96 chunks.append(urllib.parse.unquote_plus(head))
95 97 full_arg = ''.join(chunks)
96 98 pref = 'cmds='
97 99 if full_arg.startswith(pref):
98 100 # strip the cmds= header defining our batch commands
99 101 full_arg = full_arg[len(pref):]
100 102 cmds = full_arg.split(';')
101 103 return cmds
102 104
103 105 @classmethod
104 106 def _get_batch_cmd(cls, environ):
105 107 """
106 108 Handle batch command send commands. Those are ';' separated commands
107 109 sent by batch command that server needs to execute. We need to extract
108 110 those, and map them to our ACTION_MAPPING to get all push/pull commands
109 111 specified in the batch
110 112 """
111 113 default = 'push'
112 114 batch_cmds = []
113 115 try:
114 116 cmds = cls._get_xarg_headers(environ)
115 117 for pair in cmds:
116 118 parts = pair.split(' ', 1)
117 119 if len(parts) != 2:
118 120 continue
119 121 # entry should be in a format `key ARGS`
120 122 cmd, args = parts
121 123 action = cls._ACTION_MAPPING.get(cmd, default)
122 124 batch_cmds.append(action)
123 125 except Exception:
124 126 log.exception('Failed to extract batch commands operations')
125 127
126 128 # in case we failed, (e.g malformed data) assume it's PUSH sub-command
127 129 # for safety
128 130 return batch_cmds or [default]
129 131
130 132 def _get_action(self, environ):
131 133 """
132 134 Maps mercurial request commands into a pull or push command.
133 135 In case of unknown/unexpected data, it returns 'push' to be safe.
134 136
135 137 :param environ:
136 138 """
137 139 default = 'push'
138 140 query = urllib.parse.parse_qs(environ['QUERY_STRING'], keep_blank_values=True)
139 141
140 142 if 'cmd' in query:
141 143 cmd = query['cmd'][0]
142 144 if cmd == 'batch':
143 145 cmds = self._get_batch_cmd(environ)
144 146 if 'push' in cmds:
145 147 return 'push'
146 148 else:
147 149 return 'pull'
148 150 return self._ACTION_MAPPING.get(cmd, default)
149 151
150 152 return default
151 153
152 154 def _create_wsgi_app(self, repo_path, repo_name, config):
153 155 return self.scm_app.create_hg_wsgi_app(repo_path, repo_name, config)
154 156
155 157 def _create_config(self, extras, repo_name, scheme='http'):
156 158 config = utils.make_db_config(repo=repo_name)
157 159 config.set('rhodecode', 'RC_SCM_DATA', json.dumps(extras))
158 160
159 161 return config.serialize()
@@ -1,189 +1,193 b''
1 1
2 2
3 3 # Copyright (C) 2014-2020 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
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 Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 """
22 22 Implementation of the scm_app interface using raw HTTP communication.
23 23 """
24 24
25 25 import base64
26 26 import logging
27 27 import urllib.parse
28 28 import wsgiref.util
29 29
30 30 import msgpack
31 31 import requests
32 32 import webob.request
33 33
34 34 import rhodecode
35 35 from rhodecode.lib.middleware.utils import get_path_info
36 36
37 37 log = logging.getLogger(__name__)
38 38
39 39
40 40 def create_git_wsgi_app(repo_path, repo_name, config):
41 41 url = _vcs_streaming_url() + 'git/'
42 42 return VcsHttpProxy(url, repo_path, repo_name, config)
43 43
44 44
45 45 def create_hg_wsgi_app(repo_path, repo_name, config):
46 46 url = _vcs_streaming_url() + 'hg/'
47 47 return VcsHttpProxy(url, repo_path, repo_name, config)
48 48
49 49
50 50 def _vcs_streaming_url():
51 51 template = 'http://{}/stream/'
52 52 return template.format(rhodecode.CONFIG['vcs.server'])
53 53
54 54
55 55 # TODO: johbo: Avoid the global.
56 56 session = requests.Session()
57 57 # Requests speedup, avoid reading .netrc and similar
58 58 session.trust_env = False
59 59
60 60 # prevent urllib3 spawning our logs.
61 61 logging.getLogger("requests.packages.urllib3.connectionpool").setLevel(
62 62 logging.WARNING)
63 63
64 64
65 65 class VcsHttpProxy(object):
66 66 """
67 67 A WSGI application which proxies vcs requests.
68 68
69 69 The goal is to shuffle the data around without touching it. The only
70 70 exception is the extra data from the config object which we send to the
71 71 server as well.
72 72 """
73 73
74 74 def __init__(self, url, repo_path, repo_name, config):
75 75 """
76 76 :param str url: The URL of the VCSServer to call.
77 77 """
78 78 self._url = url
79 79 self._repo_name = repo_name
80 80 self._repo_path = repo_path
81 81 self._config = config
82 82 self.rc_extras = {}
83 83 log.debug(
84 84 "Creating VcsHttpProxy for repo %s, url %s",
85 85 repo_name, url)
86 86
87 87 def __call__(self, environ, start_response):
88 config = msgpack.packb(self._config)
88 config = self._config
89 89 request = webob.request.Request(environ)
90 90 request_headers = request.headers
91 91
92 request_headers.update({
92 call_context = {
93 93 # TODO: johbo: Remove this, rely on URL path only
94 'X-RC-Repo-Name': self._repo_name,
95 'X-RC-Repo-Path': self._repo_path,
96 'X-RC-Path-Info': environ['PATH_INFO'],
94 'repo_name': self._repo_name,
95 'repo_path': self._repo_path,
96 'path_info': get_path_info(environ),
97
98 'repo_store': self.rc_extras.get('repo_store'),
99 'server_config_file': self.rc_extras.get('config'),
97 100
98 'X-RC-Repo-Store': self.rc_extras.get('repo_store'),
99 'X-RC-Server-Config-File': self.rc_extras.get('config'),
101 'auth_user': self.rc_extras.get('username'),
102 'auth_user_id': str(self.rc_extras.get('user_id')),
103 'auth_user_ip': self.rc_extras.get('ip'),
100 104
101 'X-RC-Auth-User': self.rc_extras.get('username'),
102 'X-RC-Auth-User-Id': str(self.rc_extras.get('user_id')),
103 'X-RC-Auth-User-Ip': self.rc_extras.get('ip'),
105 'repo_config': config,
106 'locked_status_code': rhodecode.CONFIG.get('lock_ret_code'),
107 }
104 108
109 request_headers.update({
105 110 # TODO: johbo: Avoid encoding and put this into payload?
106 'X-RC-Repo-Config': base64.b64encode(config),
107 'X-RC-Locked-Status-Code': rhodecode.CONFIG.get('lock_ret_code'),
111 'X_RC_VCS_STREAM_CALL_CONTEXT': base64.b64encode(msgpack.packb(call_context))
108 112 })
109 113
110 114 method = environ['REQUEST_METHOD']
111 115
112 116 # Preserve the query string
113 117 url = self._url
114 118 url = urllib.parse.urljoin(url, self._repo_name)
115 119 if environ.get('QUERY_STRING'):
116 120 url += '?' + environ['QUERY_STRING']
117 121
118 122 log.debug('http-app: preparing request to: %s', url)
119 123 response = session.request(
120 124 method,
121 125 url,
122 126 data=_maybe_stream_request(environ),
123 127 headers=request_headers,
124 128 stream=True)
125 129
126 130 log.debug('http-app: got vcsserver response: %s', response)
127 131 if response.status_code >= 500:
128 132 log.error('Exception returned by vcsserver at: %s %s, %s',
129 133 url, response.status_code, response.content)
130 134
131 135 # Preserve the headers of the response, except hop_by_hop ones
132 136 response_headers = [
133 137 (h, v) for h, v in response.headers.items()
134 138 if not wsgiref.util.is_hop_by_hop(h)
135 139 ]
136 140
137 141 # Build status argument for start_response callable.
138 142 status = '{status_code} {reason_phrase}'.format(
139 143 status_code=response.status_code,
140 144 reason_phrase=response.reason)
141 145
142 146 start_response(status, response_headers)
143 147 return _maybe_stream_response(response)
144 148
145 149
146 150 def read_in_chunks(stream_obj, block_size=1024, chunks=-1):
147 151 """
148 152 Read Stream in chunks, default chunk size: 1k.
149 153 """
150 154 while chunks:
151 155 data = stream_obj.read(block_size)
152 156 if not data:
153 157 break
154 158 yield data
155 159 chunks -= 1
156 160
157 161
158 162 def _is_request_chunked(environ):
159 163 stream = environ.get('HTTP_TRANSFER_ENCODING', '') == 'chunked'
160 164 return stream
161 165
162 166
163 167 def _maybe_stream_request(environ):
164 168 path = get_path_info(environ)
165 169 stream = _is_request_chunked(environ)
166 170 log.debug('handling request `%s` with stream support: %s', path, stream)
167 171
168 172 if stream:
169 173 # set stream by 256k
170 174 return read_in_chunks(environ['wsgi.input'], block_size=1024 * 256)
171 175 else:
172 176 return environ['wsgi.input'].read()
173 177
174 178
175 179 def _maybe_stream_response(response):
176 180 """
177 181 Try to generate chunks from the response if it is chunked.
178 182 """
179 183 stream = _is_chunked(response)
180 184 log.debug('returning response with stream: %s', stream)
181 185 if stream:
182 186 # read in 256k Chunks
183 187 return response.raw.read_chunked(amt=1024 * 256)
184 188 else:
185 189 return [response.content]
186 190
187 191
188 192 def _is_chunked(response):
189 193 return response.headers.get('Transfer-Encoding', '') == 'chunked'
@@ -1,288 +1,290 b''
1 1
2 2 # Copyright (C) 2010-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 Affero General Public License, version 3
6 6 # (only), as published by the Free Software Foundation.
7 7 #
8 8 # This program is distributed in the hope that it will be useful,
9 9 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 11 # GNU General Public License for more details.
12 12 #
13 13 # You should have received a copy of the GNU Affero General Public License
14 14 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 15 #
16 16 # This program is dual-licensed. If you wish to learn more about the
17 17 # RhodeCode Enterprise Edition, including its added features, Support services,
18 18 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 19
20 20 import gzip
21 21 import shutil
22 22 import logging
23 23 import tempfile
24 24 import urllib.parse
25 25
26 26 from webob.exc import HTTPNotFound
27 27
28 28 import rhodecode
29 29 from rhodecode.lib.middleware.utils import get_path_info
30 30 from rhodecode.lib.middleware.appenlight import wrap_in_appenlight_if_enabled
31 31 from rhodecode.lib.middleware.simplegit import SimpleGit, GIT_PROTO_PAT
32 32 from rhodecode.lib.middleware.simplehg import SimpleHg
33 33 from rhodecode.lib.middleware.simplesvn import SimpleSvn
34 34 from rhodecode.model.settings import VcsSettingsModel
35 35
36 36
37 37 log = logging.getLogger(__name__)
38 38
39 39 VCS_TYPE_KEY = '_rc_vcs_type'
40 40 VCS_TYPE_SKIP = '_rc_vcs_skip'
41 41
42 42
43 43 def is_git(environ):
44 44 """
45 45 Returns True if requests should be handled by GIT wsgi middleware
46 46 """
47 47 path_info = get_path_info(environ)
48 48 is_git_path = GIT_PROTO_PAT.match(path_info)
49 49 log.debug(
50 50 'request path: `%s` detected as GIT PROTOCOL %s', path_info,
51 51 is_git_path is not None)
52 52
53 53 return is_git_path
54 54
55 55
56 56 def is_hg(environ):
57 57 """
58 58 Returns True if requests target is mercurial server - header
59 59 ``HTTP_ACCEPT`` of such request would start with ``application/mercurial``.
60 60 """
61 61 is_hg_path = False
62 62
63 63 http_accept = environ.get('HTTP_ACCEPT')
64 64
65 65 if http_accept and http_accept.startswith('application/mercurial'):
66 66 query = urllib.parse.parse_qs(environ['QUERY_STRING'])
67 67 if 'cmd' in query:
68 68 is_hg_path = True
69 69
70 70 path_info = get_path_info(environ)
71 71 log.debug(
72 72 'request path: `%s` detected as HG PROTOCOL %s', path_info,
73 73 is_hg_path)
74 74
75 75 return is_hg_path
76 76
77 77
78 78 def is_svn(environ):
79 79 """
80 80 Returns True if requests target is Subversion server
81 81 """
82 82
83 83 http_dav = environ.get('HTTP_DAV', '')
84 84 magic_path_segment = rhodecode.CONFIG.get(
85 85 'rhodecode_subversion_magic_path', '/!svn')
86 86 path_info = get_path_info(environ)
87 87 is_svn_path = (
88 88 'subversion' in http_dav or
89 89 magic_path_segment in path_info
90 90 or environ['REQUEST_METHOD'] in ['PROPFIND', 'PROPPATCH']
91 91 )
92 92 log.debug(
93 93 'request path: `%s` detected as SVN PROTOCOL %s', path_info,
94 94 is_svn_path)
95 95
96 96 return is_svn_path
97 97
98 98
99 99 class GunzipMiddleware(object):
100 100 """
101 101 WSGI middleware that unzips gzip-encoded requests before
102 102 passing on to the underlying application.
103 103 """
104 104
105 105 def __init__(self, application):
106 106 self.app = application
107 107
108 108 def __call__(self, environ, start_response):
109 109 accepts_encoding_header = environ.get('HTTP_CONTENT_ENCODING', b'')
110 110
111 111 if b'gzip' in accepts_encoding_header:
112 112 log.debug('gzip detected, now running gunzip wrapper')
113 113 wsgi_input = environ['wsgi.input']
114 114
115 115 if not hasattr(environ['wsgi.input'], 'seek'):
116 116 # The gzip implementation in the standard library of Python 2.x
117 117 # requires the '.seek()' and '.tell()' methods to be available
118 118 # on the input stream. Read the data into a temporary file to
119 119 # work around this limitation.
120 120
121 121 wsgi_input = tempfile.SpooledTemporaryFile(64 * 1024 * 1024)
122 122 shutil.copyfileobj(environ['wsgi.input'], wsgi_input)
123 123 wsgi_input.seek(0)
124 124
125 125 environ['wsgi.input'] = gzip.GzipFile(fileobj=wsgi_input, mode='r')
126 126 # since we "Ungzipped" the content we say now it's no longer gzip
127 127 # content encoding
128 128 del environ['HTTP_CONTENT_ENCODING']
129 129
130 130 # content length has changes ? or i'm not sure
131 131 if 'CONTENT_LENGTH' in environ:
132 132 del environ['CONTENT_LENGTH']
133 133 else:
134 134 log.debug('content not gzipped, gzipMiddleware passing '
135 135 'request further')
136 136 return self.app(environ, start_response)
137 137
138 138
139 139 def is_vcs_call(environ):
140 140 if VCS_TYPE_KEY in environ:
141 141 raw_type = environ[VCS_TYPE_KEY]
142 142 return raw_type and raw_type != VCS_TYPE_SKIP
143 143 return False
144 144
145 145
146 def get_path_elem(route_path):
147 if not route_path:
148 return None
149
150 cleaned_route_path = route_path.lstrip('/')
151 if cleaned_route_path:
152 cleaned_route_path_elems = cleaned_route_path.split('/')
153 if cleaned_route_path_elems:
154 return cleaned_route_path_elems[0]
155 return None
156
157
158 146 def detect_vcs_request(environ, backends):
159 147 checks = {
160 148 'hg': (is_hg, SimpleHg),
161 149 'git': (is_git, SimpleGit),
162 150 'svn': (is_svn, SimpleSvn),
163 151 }
164 152 handler = None
165 153 # List of path views first chunk we don't do any checks
166 154 white_list = [
155 # favicon often requested by browsers
156 'favicon.ico',
157
167 158 # e.g /_file_store/download
168 '_file_store',
159 '_file_store++',
160
161 # _admin/api is safe too
162 '_admin/api',
163
164 # _admin/gist is safe too
165 '_admin/gists++',
166
167 # _admin/my_account is safe too
168 '_admin/my_account++',
169 169
170 170 # static files no detection
171 '_static',
171 '_static++',
172
173 # debug-toolbar
174 '_debug_toolbar++',
172 175
173 176 # skip ops ping, status
174 177 '_admin/ops/ping',
175 178 '_admin/ops/status',
176 179
177 180 # full channelstream connect should be VCS skipped
178 181 '_admin/channelstream/connect',
179 182 ]
180 183 path_info = get_path_info(environ)
181 184 path_url = path_info.lstrip('/')
182 185
183 if path_elem in white_list:
184 log.debug('path `%s` in whitelist, skipping...', path_info)
186 for item in white_list:
187 if item.endswith('++') and path_url.startswith(item[:-2]):
188 log.debug('path `%s` in whitelist (match:%s), skipping...', path_url, item)
185 189 return handler
186
187 path_url = path_info.lstrip('/')
188 if path_url in white_list:
189 log.debug('full url path `%s` in whitelist, skipping...', path_url)
190 if item == path_url:
191 log.debug('path `%s` in whitelist (match:%s), skipping...', path_url, item)
190 192 return handler
191 193
192 194 if VCS_TYPE_KEY in environ:
193 195 raw_type = environ[VCS_TYPE_KEY]
194 196 if raw_type == VCS_TYPE_SKIP:
195 197 log.debug('got `skip` marker for vcs detection, skipping...')
196 198 return handler
197 199
198 200 _check, handler = checks.get(raw_type) or [None, None]
199 201 if handler:
200 202 log.debug('got handler:%s from environ', handler)
201 203
202 204 if not handler:
203 log.debug('request start: checking if request for `%s` is of VCS type in order: %s', path_elem, backends)
205 log.debug('request start: checking if request for `%s` is of VCS type in order: %s', path_url, backends)
204 206 for vcs_type in backends:
205 207 vcs_check, _handler = checks[vcs_type]
206 208 if vcs_check(environ):
207 209 log.debug('vcs handler found %s', _handler)
208 210 handler = _handler
209 211 break
210 212
211 213 return handler
212 214
213 215
214 216 class VCSMiddleware(object):
215 217
216 218 def __init__(self, app, registry, config, appenlight_client):
217 219 self.application = app
218 220 self.registry = registry
219 221 self.config = config
220 222 self.appenlight_client = appenlight_client
221 223 self.use_gzip = True
222 224 # order in which we check the middlewares, based on vcs.backends config
223 225 self.check_middlewares = config['vcs.backends']
224 226
225 227 def vcs_config(self, repo_name=None):
226 228 """
227 229 returns serialized VcsSettings
228 230 """
229 231 try:
230 232 return VcsSettingsModel(
231 233 repo=repo_name).get_ui_settings_as_config_obj()
232 234 except Exception:
233 235 pass
234 236
235 237 def wrap_in_gzip_if_enabled(self, app, config):
236 238 if self.use_gzip:
237 239 app = GunzipMiddleware(app)
238 240 return app
239 241
240 242 def _get_handler_app(self, environ):
241 243 app = None
242 244 log.debug('VCSMiddleware: detecting vcs type.')
243 245 handler = detect_vcs_request(environ, self.check_middlewares)
244 246 if handler:
245 247 app = handler(self.config, self.registry)
246 248
247 249 return app
248 250
249 251 def __call__(self, environ, start_response):
250 252 # check if we handle one of interesting protocols, optionally extract
251 253 # specific vcsSettings and allow changes of how things are wrapped
252 254 vcs_handler = self._get_handler_app(environ)
253 255 if vcs_handler:
254 256 # translate the _REPO_ID into real repo NAME for usage
255 257 # in middleware
256 258
257 259 path_info = get_path_info(environ)
258 260 environ['PATH_INFO'] = vcs_handler._get_by_id(path_info)
259 261
260 262 # Set acl, url and vcs repo names.
261 263 vcs_handler.set_repo_names(environ)
262 264
263 265 # register repo config back to the handler
264 266 vcs_conf = self.vcs_config(vcs_handler.acl_repo_name)
265 267 # maybe damaged/non existent settings. We still want to
266 268 # pass that point to validate on is_valid_and_existing_repo
267 269 # and return proper HTTP Code back to client
268 270 if vcs_conf:
269 271 vcs_handler.repo_vcs_config = vcs_conf
270 272
271 273 # check for type, presence in database and on filesystem
272 274 if not vcs_handler.is_valid_and_existing_repo(
273 275 vcs_handler.acl_repo_name,
274 276 vcs_handler.base_path,
275 277 vcs_handler.SCM):
276 278 return HTTPNotFound()(environ, start_response)
277 279
278 280 environ['REPO_NAME'] = vcs_handler.url_repo_name
279 281
280 282 # Wrap handler in middlewares if they are enabled.
281 283 vcs_handler = self.wrap_in_gzip_if_enabled(
282 284 vcs_handler, self.config)
283 285 vcs_handler, _ = wrap_in_appenlight_if_enabled(
284 286 vcs_handler, self.config, self.appenlight_client)
285 287
286 288 return vcs_handler(environ, start_response)
287 289
288 290 return self.application(environ, start_response)
General Comments 0
You need to be logged in to leave comments. Login now