##// END OF EJS Templates
feat(svn): improvements to handle SVN protocol 1.4 features...
super-admin -
r5215:e17d6d15 default
parent child Browse files
Show More
@@ -1,231 +1,239 b''
1
1
2 # Copyright (C) 2010-2023 RhodeCode GmbH
2 # Copyright (C) 2010-2023 RhodeCode GmbH
3 #
3 #
4 # This program is free software: you can redistribute it and/or modify
4 # This program is free software: you can redistribute it and/or modify
5 # it under the terms of the GNU Affero General Public License, version 3
5 # it under the terms of the GNU Affero General Public License, version 3
6 # (only), as published by the Free Software Foundation.
6 # (only), as published by the Free Software Foundation.
7 #
7 #
8 # This program is distributed in the hope that it will be useful,
8 # This program is distributed in the hope that it will be useful,
9 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # GNU General Public License for more details.
11 # GNU General Public License for more details.
12 #
12 #
13 # You should have received a copy of the GNU Affero General Public License
13 # You should have received a copy of the GNU Affero General Public License
14 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 #
15 #
16 # This program is dual-licensed. If you wish to learn more about the
16 # This program is dual-licensed. If you wish to learn more about the
17 # RhodeCode Enterprise Edition, including its added features, Support services,
17 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # and proprietary license terms, please see https://rhodecode.com/licenses/
18 # and proprietary license terms, please see https://rhodecode.com/licenses/
19
19
20 import base64
20 import base64
21 import logging
21 import logging
22 import urllib.request
22 import urllib.request
23 import urllib.parse
23 import urllib.parse
24 import urllib.error
24 import urllib.error
25 import urllib.parse
25 import urllib.parse
26
26
27 import requests
27 import requests
28 from pyramid.httpexceptions import HTTPNotAcceptable
28 from pyramid.httpexceptions import HTTPNotAcceptable
29
29
30 from rhodecode.lib import rc_cache
30 from rhodecode.lib import rc_cache
31 from rhodecode.lib.middleware import simplevcs
31 from rhodecode.lib.middleware import simplevcs
32 from rhodecode.lib.middleware.utils import get_path_info
32 from rhodecode.lib.middleware.utils import get_path_info
33 from rhodecode.lib.utils import is_valid_repo
33 from rhodecode.lib.utils import is_valid_repo
34 from rhodecode.lib.str_utils import safe_str, safe_int, safe_bytes
34 from rhodecode.lib.str_utils import safe_str, safe_int, safe_bytes
35 from rhodecode.lib.type_utils import str2bool
35 from rhodecode.lib.type_utils import str2bool
36 from rhodecode.lib.ext_json import json
36 from rhodecode.lib.ext_json import json
37 from rhodecode.lib.hooks_daemon import store_txn_id_data
37 from rhodecode.lib.hooks_daemon import store_txn_id_data
38
38
39
39
40 log = logging.getLogger(__name__)
40 log = logging.getLogger(__name__)
41
41
42
42
43 class SimpleSvnApp(object):
43 class SimpleSvnApp(object):
44 IGNORED_HEADERS = [
44 IGNORED_HEADERS = [
45 'connection', 'keep-alive', 'content-encoding',
45 'connection', 'keep-alive', 'content-encoding',
46 'transfer-encoding', 'content-length']
46 'transfer-encoding', 'content-length']
47 rc_extras = {}
47 rc_extras = {}
48
48
49 def __init__(self, config):
49 def __init__(self, config):
50 self.config = config
50 self.config = config
51 self.session = requests.Session()
51
52
52 def __call__(self, environ, start_response):
53 def __call__(self, environ, start_response):
53 request_headers = self._get_request_headers(environ)
54 request_headers = self._get_request_headers(environ)
54 data_io = environ['wsgi.input']
55 data_io = environ['wsgi.input']
55 req_method: str = environ['REQUEST_METHOD']
56 req_method: str = environ['REQUEST_METHOD']
56 has_content_length = 'CONTENT_LENGTH' in environ
57 has_content_length = 'CONTENT_LENGTH' in environ
57
58
58 path_info = self._get_url(
59 path_info = self._get_url(
59 self.config.get('subversion_http_server_url', ''), get_path_info(environ))
60 self.config.get('subversion_http_server_url', ''), get_path_info(environ))
60 transfer_encoding = environ.get('HTTP_TRANSFER_ENCODING', '')
61 transfer_encoding = environ.get('HTTP_TRANSFER_ENCODING', '')
61 log.debug('Handling: %s method via `%s`', req_method, path_info)
62 log.debug('Handling: %s method via `%s`', req_method, path_info)
62
63
63 # stream control flag, based on request and content type...
64 # stream control flag, based on request and content type...
64 stream = False
65 stream = False
65
66
66 if req_method in ['MKCOL'] or has_content_length:
67 if req_method in ['MKCOL'] or has_content_length:
67 data_processed = False
68 data_processed = False
68 # read chunk to check if we have txn-with-props
69 # read chunk to check if we have txn-with-props
69 initial_data: bytes = data_io.read(1024)
70 initial_data: bytes = data_io.read(1024)
70 if initial_data.startswith(b'(create-txn-with-props'):
71 if initial_data.startswith(b'(create-txn-with-props'):
71 data_io = initial_data + data_io.read()
72 data_io = initial_data + data_io.read()
72 # store on-the-fly our rc_extra using svn revision properties
73 # store on-the-fly our rc_extra using svn revision properties
73 # those can be read later on in hooks executed so we have a way
74 # those can be read later on in hooks executed so we have a way
74 # to pass in the data into svn hooks
75 # to pass in the data into svn hooks
75 rc_data = base64.urlsafe_b64encode(json.dumps(self.rc_extras))
76 rc_data = base64.urlsafe_b64encode(json.dumps(self.rc_extras))
76 rc_data_len = str(len(rc_data))
77 rc_data_len = str(len(rc_data))
77 # header defines data length, and serialized data
78 # header defines data length, and serialized data
78 skel = b' rc-scm-extras %b %b' % (safe_bytes(rc_data_len), safe_bytes(rc_data))
79 skel = b' rc-scm-extras %b %b' % (safe_bytes(rc_data_len), safe_bytes(rc_data))
79 data_io = data_io[:-2] + skel + b'))'
80 data_io = data_io[:-2] + skel + b'))'
80 data_processed = True
81 data_processed = True
81
82
82 if not data_processed:
83 if not data_processed:
83 # NOTE(johbo): Avoid that we end up with sending the request in chunked
84 # NOTE(johbo): Avoid that we end up with sending the request in chunked
84 # transfer encoding (mainly on Gunicorn). If we know the content
85 # transfer encoding (mainly on Gunicorn). If we know the content
85 # length, then we should transfer the payload in one request.
86 # length, then we should transfer the payload in one request.
86 data_io = initial_data + data_io.read()
87 data_io = initial_data + data_io.read()
87
88
88 if req_method in ['GET', 'PUT'] or transfer_encoding == 'chunked':
89 if req_method in ['GET', 'PUT'] or transfer_encoding == 'chunked':
89 # NOTE(marcink): when getting/uploading files, we want to STREAM content
90 # NOTE(marcink): when getting/uploading files, we want to STREAM content
90 # back to the client/proxy instead of buffering it here...
91 # back to the client/proxy instead of buffering it here...
91 stream = True
92 stream = True
92
93
93 stream = stream
94 stream = stream
94 log.debug('Calling SVN PROXY at `%s`, using method:%s. Stream: %s',
95 log.debug('Calling SVN PROXY at `%s`, using method:%s. Stream: %s',
95 path_info, req_method, stream)
96 path_info, req_method, stream)
96
97
98 call_kwargs = dict(
99 data=data_io,
100 headers=request_headers,
101 stream=stream
102 )
103 if req_method in ['HEAD', 'DELETE']:
104 del call_kwargs['data']
105
97 try:
106 try:
98 response = requests.request(
107 response = self.session.request(
99 req_method, path_info,
108 req_method, path_info, **call_kwargs)
100 data=data_io, headers=request_headers, stream=stream)
101 except requests.ConnectionError:
109 except requests.ConnectionError:
102 log.exception('ConnectionError occurred for endpoint %s', path_info)
110 log.exception('ConnectionError occurred for endpoint %s', path_info)
103 raise
111 raise
104
112
105 if response.status_code not in [200, 401]:
113 if response.status_code not in [200, 401]:
106 text = '\n{}'.format(safe_str(response.text)) if response.text else ''
114 text = '\n{}'.format(safe_str(response.text)) if response.text else ''
107 if response.status_code >= 500:
115 if response.status_code >= 500:
108 log.error('Got SVN response:%s with text:`%s`', response, text)
116 log.error('Got SVN response:%s with text:`%s`', response, text)
109 else:
117 else:
110 log.debug('Got SVN response:%s with text:`%s`', response, text)
118 log.debug('Got SVN response:%s with text:`%s`', response, text)
111 else:
119 else:
112 log.debug('got response code: %s', response.status_code)
120 log.debug('got response code: %s', response.status_code)
113
121
114 response_headers = self._get_response_headers(response.headers)
122 response_headers = self._get_response_headers(response.headers)
115
123
116 if response.headers.get('SVN-Txn-name'):
124 if response.headers.get('SVN-Txn-name'):
117 svn_tx_id = response.headers.get('SVN-Txn-name')
125 svn_tx_id = response.headers.get('SVN-Txn-name')
118 txn_id = rc_cache.utils.compute_key_from_params(
126 txn_id = rc_cache.utils.compute_key_from_params(
119 self.config['repository'], svn_tx_id)
127 self.config['repository'], svn_tx_id)
120 port = safe_int(self.rc_extras['hooks_uri'].split(':')[-1])
128 port = safe_int(self.rc_extras['hooks_uri'].split(':')[-1])
121 store_txn_id_data(txn_id, {'port': port})
129 store_txn_id_data(txn_id, {'port': port})
122
130
123 start_response(f'{response.status_code} {response.reason}', response_headers)
131 start_response(f'{response.status_code} {response.reason}', response_headers)
124 return response.iter_content(chunk_size=1024)
132 return response.iter_content(chunk_size=1024)
125
133
126 def _get_url(self, svn_http_server, path):
134 def _get_url(self, svn_http_server, path):
127 svn_http_server_url = (svn_http_server or '').rstrip('/')
135 svn_http_server_url = (svn_http_server or '').rstrip('/')
128 url_path = urllib.parse.urljoin(svn_http_server_url + '/', (path or '').lstrip('/'))
136 url_path = urllib.parse.urljoin(svn_http_server_url + '/', (path or '').lstrip('/'))
129 url_path = urllib.parse.quote(url_path, safe="/:=~+!$,;'")
137 url_path = urllib.parse.quote(url_path, safe="/:=~+!$,;'")
130 return url_path
138 return url_path
131
139
132 def _get_request_headers(self, environ):
140 def _get_request_headers(self, environ):
133 headers = {}
141 headers = {}
134
142
135 for key in environ:
143 for key in environ:
136 if not key.startswith('HTTP_'):
144 if not key.startswith('HTTP_'):
137 continue
145 continue
138 new_key = key.split('_')
146 new_key = key.split('_')
139 new_key = [k.capitalize() for k in new_key[1:]]
147 new_key = [k.capitalize() for k in new_key[1:]]
140 new_key = '-'.join(new_key)
148 new_key = '-'.join(new_key)
141 headers[new_key] = environ[key]
149 headers[new_key] = environ[key]
142
150
143 if 'CONTENT_TYPE' in environ:
151 if 'CONTENT_TYPE' in environ:
144 headers['Content-Type'] = environ['CONTENT_TYPE']
152 headers['Content-Type'] = environ['CONTENT_TYPE']
145
153
146 if 'CONTENT_LENGTH' in environ:
154 if 'CONTENT_LENGTH' in environ:
147 headers['Content-Length'] = environ['CONTENT_LENGTH']
155 headers['Content-Length'] = environ['CONTENT_LENGTH']
148
156
149 return headers
157 return headers
150
158
151 def _get_response_headers(self, headers):
159 def _get_response_headers(self, headers):
152 headers = [
160 headers = [
153 (h, headers[h])
161 (h, headers[h])
154 for h in headers
162 for h in headers
155 if h.lower() not in self.IGNORED_HEADERS
163 if h.lower() not in self.IGNORED_HEADERS
156 ]
164 ]
157
165
158 return headers
166 return headers
159
167
160
168
161 class DisabledSimpleSvnApp(object):
169 class DisabledSimpleSvnApp(object):
162 def __init__(self, config):
170 def __init__(self, config):
163 self.config = config
171 self.config = config
164
172
165 def __call__(self, environ, start_response):
173 def __call__(self, environ, start_response):
166 reason = 'Cannot handle SVN call because: SVN HTTP Proxy is not enabled'
174 reason = 'Cannot handle SVN call because: SVN HTTP Proxy is not enabled'
167 log.warning(reason)
175 log.warning(reason)
168 return HTTPNotAcceptable(reason)(environ, start_response)
176 return HTTPNotAcceptable(reason)(environ, start_response)
169
177
170
178
171 class SimpleSvn(simplevcs.SimpleVCS):
179 class SimpleSvn(simplevcs.SimpleVCS):
172
180
173 SCM = 'svn'
181 SCM = 'svn'
174 READ_ONLY_COMMANDS = ('OPTIONS', 'PROPFIND', 'GET', 'REPORT')
182 READ_ONLY_COMMANDS = ('OPTIONS', 'PROPFIND', 'GET', 'REPORT')
175 DEFAULT_HTTP_SERVER = 'http://localhost:8090'
183 DEFAULT_HTTP_SERVER = 'http://localhost:8090'
176
184
177 def _get_repository_name(self, environ):
185 def _get_repository_name(self, environ):
178 """
186 """
179 Gets repository name out of PATH_INFO header
187 Gets repository name out of PATH_INFO header
180
188
181 :param environ: environ where PATH_INFO is stored
189 :param environ: environ where PATH_INFO is stored
182 """
190 """
183 path = get_path_info(environ).split('!')
191 path = get_path_info(environ).split('!')
184 repo_name = path[0].strip('/')
192 repo_name = path[0].strip('/')
185
193
186 # SVN includes the whole path in it's requests, including
194 # SVN includes the whole path in it's requests, including
187 # subdirectories inside the repo. Therefore we have to search for
195 # subdirectories inside the repo. Therefore we have to search for
188 # the repo root directory.
196 # the repo root directory.
189 if not is_valid_repo(
197 if not is_valid_repo(
190 repo_name, self.base_path, explicit_scm=self.SCM):
198 repo_name, self.base_path, explicit_scm=self.SCM):
191 current_path = ''
199 current_path = ''
192 for component in repo_name.split('/'):
200 for component in repo_name.split('/'):
193 current_path += component
201 current_path += component
194 if is_valid_repo(
202 if is_valid_repo(
195 current_path, self.base_path, explicit_scm=self.SCM):
203 current_path, self.base_path, explicit_scm=self.SCM):
196 return current_path
204 return current_path
197 current_path += '/'
205 current_path += '/'
198
206
199 return repo_name
207 return repo_name
200
208
201 def _get_action(self, environ):
209 def _get_action(self, environ):
202 return (
210 return (
203 'pull'
211 'pull'
204 if environ['REQUEST_METHOD'] in self.READ_ONLY_COMMANDS
212 if environ['REQUEST_METHOD'] in self.READ_ONLY_COMMANDS
205 else 'push')
213 else 'push')
206
214
207 def _should_use_callback_daemon(self, extras, environ, action):
215 def _should_use_callback_daemon(self, extras, environ, action):
208 # only MERGE command triggers hooks, so we don't want to start
216 # only MERGE command triggers hooks, so we don't want to start
209 # hooks server too many times. POST however starts the svn transaction
217 # hooks server too many times. POST however starts the svn transaction
210 # so we also need to run the init of callback daemon of POST
218 # so we also need to run the init of callback daemon of POST
211 if environ['REQUEST_METHOD'] in ['MERGE', 'POST']:
219 if environ['REQUEST_METHOD'] in ['MERGE', 'POST']:
212 return True
220 return True
213 return False
221 return False
214
222
215 def _create_wsgi_app(self, repo_path, repo_name, config):
223 def _create_wsgi_app(self, repo_path, repo_name, config):
216 if self._is_svn_enabled():
224 if self._is_svn_enabled():
217 return SimpleSvnApp(config)
225 return SimpleSvnApp(config)
218 # we don't have http proxy enabled return dummy request handler
226 # we don't have http proxy enabled return dummy request handler
219 return DisabledSimpleSvnApp(config)
227 return DisabledSimpleSvnApp(config)
220
228
221 def _is_svn_enabled(self):
229 def _is_svn_enabled(self):
222 conf = self.repo_vcs_config
230 conf = self.repo_vcs_config
223 return str2bool(conf.get('vcs_svn_proxy', 'http_requests_enabled'))
231 return str2bool(conf.get('vcs_svn_proxy', 'http_requests_enabled'))
224
232
225 def _create_config(self, extras, repo_name, scheme='http'):
233 def _create_config(self, extras, repo_name, scheme='http'):
226 conf = self.repo_vcs_config
234 conf = self.repo_vcs_config
227 server_url = conf.get('vcs_svn_proxy', 'http_server_url')
235 server_url = conf.get('vcs_svn_proxy', 'http_server_url')
228 server_url = server_url or self.DEFAULT_HTTP_SERVER
236 server_url = server_url or self.DEFAULT_HTTP_SERVER
229
237
230 extras['subversion_http_server_url'] = server_url
238 extras['subversion_http_server_url'] = server_url
231 return extras
239 return extras
@@ -1,700 +1,701 b''
1
1
2
2
3 # Copyright (C) 2014-2023 RhodeCode GmbH
3 # Copyright (C) 2014-2023 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 SimpleVCS middleware for handling protocol request (push/clone etc.)
22 SimpleVCS middleware for handling protocol request (push/clone etc.)
23 It's implemented with basic auth function
23 It's implemented with basic auth function
24 """
24 """
25
25
26 import os
26 import os
27 import re
27 import re
28 import io
28 import io
29 import logging
29 import logging
30 import importlib
30 import importlib
31 from functools import wraps
31 from functools import wraps
32 from lxml import etree
32 from lxml import etree
33
33
34 import time
34 import time
35 from paste.httpheaders import REMOTE_USER, AUTH_TYPE
35 from paste.httpheaders import REMOTE_USER, AUTH_TYPE
36
36
37 from pyramid.httpexceptions import (
37 from pyramid.httpexceptions import (
38 HTTPNotFound, HTTPForbidden, HTTPNotAcceptable, HTTPInternalServerError)
38 HTTPNotFound, HTTPForbidden, HTTPNotAcceptable, HTTPInternalServerError)
39 from zope.cachedescriptors.property import Lazy as LazyProperty
39 from zope.cachedescriptors.property import Lazy as LazyProperty
40
40
41 import rhodecode
41 import rhodecode
42 from rhodecode.authentication.base import authenticate, VCS_TYPE, loadplugin
42 from rhodecode.authentication.base import authenticate, VCS_TYPE, loadplugin
43 from rhodecode.lib import rc_cache
43 from rhodecode.lib import rc_cache
44 from rhodecode.lib.auth import AuthUser, HasPermissionAnyMiddleware
44 from rhodecode.lib.auth import AuthUser, HasPermissionAnyMiddleware
45 from rhodecode.lib.base import (
45 from rhodecode.lib.base import (
46 BasicAuth, get_ip_addr, get_user_agent, vcs_operation_context)
46 BasicAuth, get_ip_addr, get_user_agent, vcs_operation_context)
47 from rhodecode.lib.exceptions import (UserCreationError, NotAllowedToCreateUserError)
47 from rhodecode.lib.exceptions import (UserCreationError, NotAllowedToCreateUserError)
48 from rhodecode.lib.hooks_daemon import prepare_callback_daemon
48 from rhodecode.lib.hooks_daemon import prepare_callback_daemon
49 from rhodecode.lib.middleware import appenlight
49 from rhodecode.lib.middleware import appenlight
50 from rhodecode.lib.middleware.utils import scm_app_http
50 from rhodecode.lib.middleware.utils import scm_app_http
51 from rhodecode.lib.str_utils import safe_bytes
51 from rhodecode.lib.str_utils import safe_bytes
52 from rhodecode.lib.utils import is_valid_repo, SLUG_RE
52 from rhodecode.lib.utils import is_valid_repo, SLUG_RE
53 from rhodecode.lib.utils2 import safe_str, fix_PATH, str2bool
53 from rhodecode.lib.utils2 import safe_str, fix_PATH, str2bool
54 from rhodecode.lib.vcs.conf import settings as vcs_settings
54 from rhodecode.lib.vcs.conf import settings as vcs_settings
55 from rhodecode.lib.vcs.backends import base
55 from rhodecode.lib.vcs.backends import base
56
56
57 from rhodecode.model import meta
57 from rhodecode.model import meta
58 from rhodecode.model.db import User, Repository, PullRequest
58 from rhodecode.model.db import User, Repository, PullRequest
59 from rhodecode.model.scm import ScmModel
59 from rhodecode.model.scm import ScmModel
60 from rhodecode.model.pull_request import PullRequestModel
60 from rhodecode.model.pull_request import PullRequestModel
61 from rhodecode.model.settings import SettingsModel, VcsSettingsModel
61 from rhodecode.model.settings import SettingsModel, VcsSettingsModel
62
62
63 log = logging.getLogger(__name__)
63 log = logging.getLogger(__name__)
64
64
65
65
66 def extract_svn_txn_id(acl_repo_name, data):
66 def extract_svn_txn_id(acl_repo_name, data: bytes):
67 """
67 """
68 Helper method for extraction of svn txn_id from submitted XML data during
68 Helper method for extraction of svn txn_id from submitted XML data during
69 POST operations
69 POST operations
70 """
70 """
71
71 try:
72 try:
72 root = etree.fromstring(data)
73 root = etree.fromstring(data)
73 pat = re.compile(r'/txn/(?P<txn_id>.*)')
74 pat = re.compile(r'/txn/(?P<txn_id>.*)')
74 for el in root:
75 for el in root:
75 if el.tag == '{DAV:}source':
76 if el.tag == '{DAV:}source':
76 for sub_el in el:
77 for sub_el in el:
77 if sub_el.tag == '{DAV:}href':
78 if sub_el.tag == '{DAV:}href':
78 match = pat.search(sub_el.text)
79 match = pat.search(sub_el.text)
79 if match:
80 if match:
80 svn_tx_id = match.groupdict()['txn_id']
81 svn_tx_id = match.groupdict()['txn_id']
81 txn_id = rc_cache.utils.compute_key_from_params(
82 txn_id = rc_cache.utils.compute_key_from_params(
82 acl_repo_name, svn_tx_id)
83 acl_repo_name, svn_tx_id)
83 return txn_id
84 return txn_id
84 except Exception:
85 except Exception:
85 log.exception('Failed to extract txn_id')
86 log.exception('Failed to extract txn_id')
86
87
87
88
88 def initialize_generator(factory):
89 def initialize_generator(factory):
89 """
90 """
90 Initializes the returned generator by draining its first element.
91 Initializes the returned generator by draining its first element.
91
92
92 This can be used to give a generator an initializer, which is the code
93 This can be used to give a generator an initializer, which is the code
93 up to the first yield statement. This decorator enforces that the first
94 up to the first yield statement. This decorator enforces that the first
94 produced element has the value ``"__init__"`` to make its special
95 produced element has the value ``"__init__"`` to make its special
95 purpose very explicit in the using code.
96 purpose very explicit in the using code.
96 """
97 """
97
98
98 @wraps(factory)
99 @wraps(factory)
99 def wrapper(*args, **kwargs):
100 def wrapper(*args, **kwargs):
100 gen = factory(*args, **kwargs)
101 gen = factory(*args, **kwargs)
101 try:
102 try:
102 init = next(gen)
103 init = next(gen)
103 except StopIteration:
104 except StopIteration:
104 raise ValueError('Generator must yield at least one element.')
105 raise ValueError('Generator must yield at least one element.')
105 if init != "__init__":
106 if init != "__init__":
106 raise ValueError('First yielded element must be "__init__".')
107 raise ValueError('First yielded element must be "__init__".')
107 return gen
108 return gen
108 return wrapper
109 return wrapper
109
110
110
111
111 class SimpleVCS(object):
112 class SimpleVCS(object):
112 """Common functionality for SCM HTTP handlers."""
113 """Common functionality for SCM HTTP handlers."""
113
114
114 SCM = 'unknown'
115 SCM = 'unknown'
115
116
116 acl_repo_name = None
117 acl_repo_name = None
117 url_repo_name = None
118 url_repo_name = None
118 vcs_repo_name = None
119 vcs_repo_name = None
119 rc_extras = {}
120 rc_extras = {}
120
121
121 # We have to handle requests to shadow repositories different than requests
122 # We have to handle requests to shadow repositories different than requests
122 # to normal repositories. Therefore we have to distinguish them. To do this
123 # to normal repositories. Therefore we have to distinguish them. To do this
123 # we use this regex which will match only on URLs pointing to shadow
124 # we use this regex which will match only on URLs pointing to shadow
124 # repositories.
125 # repositories.
125 shadow_repo_re = re.compile(
126 shadow_repo_re = re.compile(
126 '(?P<groups>(?:{slug_pat}/)*)' # repo groups
127 '(?P<groups>(?:{slug_pat}/)*)' # repo groups
127 '(?P<target>{slug_pat})/' # target repo
128 '(?P<target>{slug_pat})/' # target repo
128 'pull-request/(?P<pr_id>\\d+)/' # pull request
129 'pull-request/(?P<pr_id>\\d+)/' # pull request
129 'repository$' # shadow repo
130 'repository$' # shadow repo
130 .format(slug_pat=SLUG_RE.pattern))
131 .format(slug_pat=SLUG_RE.pattern))
131
132
132 def __init__(self, config, registry):
133 def __init__(self, config, registry):
133 self.registry = registry
134 self.registry = registry
134 self.config = config
135 self.config = config
135 # re-populated by specialized middleware
136 # re-populated by specialized middleware
136 self.repo_vcs_config = base.Config()
137 self.repo_vcs_config = base.Config()
137
138
138 rc_settings = SettingsModel().get_all_settings(cache=True, from_request=False)
139 rc_settings = SettingsModel().get_all_settings(cache=True, from_request=False)
139 realm = rc_settings.get('rhodecode_realm') or 'RhodeCode AUTH'
140 realm = rc_settings.get('rhodecode_realm') or 'RhodeCode AUTH'
140
141
141 # authenticate this VCS request using authfunc
142 # authenticate this VCS request using authfunc
142 auth_ret_code_detection = \
143 auth_ret_code_detection = \
143 str2bool(self.config.get('auth_ret_code_detection', False))
144 str2bool(self.config.get('auth_ret_code_detection', False))
144 self.authenticate = BasicAuth(
145 self.authenticate = BasicAuth(
145 '', authenticate, registry, config.get('auth_ret_code'),
146 '', authenticate, registry, config.get('auth_ret_code'),
146 auth_ret_code_detection, rc_realm=realm)
147 auth_ret_code_detection, rc_realm=realm)
147 self.ip_addr = '0.0.0.0'
148 self.ip_addr = '0.0.0.0'
148
149
149 @LazyProperty
150 @LazyProperty
150 def global_vcs_config(self):
151 def global_vcs_config(self):
151 try:
152 try:
152 return VcsSettingsModel().get_ui_settings_as_config_obj()
153 return VcsSettingsModel().get_ui_settings_as_config_obj()
153 except Exception:
154 except Exception:
154 return base.Config()
155 return base.Config()
155
156
156 @property
157 @property
157 def base_path(self):
158 def base_path(self):
158 settings_path = self.repo_vcs_config.get(*VcsSettingsModel.PATH_SETTING)
159 settings_path = self.repo_vcs_config.get(*VcsSettingsModel.PATH_SETTING)
159
160
160 if not settings_path:
161 if not settings_path:
161 settings_path = self.global_vcs_config.get(*VcsSettingsModel.PATH_SETTING)
162 settings_path = self.global_vcs_config.get(*VcsSettingsModel.PATH_SETTING)
162
163
163 if not settings_path:
164 if not settings_path:
164 # try, maybe we passed in explicitly as config option
165 # try, maybe we passed in explicitly as config option
165 settings_path = self.config.get('base_path')
166 settings_path = self.config.get('base_path')
166
167
167 if not settings_path:
168 if not settings_path:
168 raise ValueError('FATAL: base_path is empty')
169 raise ValueError('FATAL: base_path is empty')
169 return settings_path
170 return settings_path
170
171
171 def set_repo_names(self, environ):
172 def set_repo_names(self, environ):
172 """
173 """
173 This will populate the attributes acl_repo_name, url_repo_name,
174 This will populate the attributes acl_repo_name, url_repo_name,
174 vcs_repo_name and is_shadow_repo. In case of requests to normal (non
175 vcs_repo_name and is_shadow_repo. In case of requests to normal (non
175 shadow) repositories all names are equal. In case of requests to a
176 shadow) repositories all names are equal. In case of requests to a
176 shadow repository the acl-name points to the target repo of the pull
177 shadow repository the acl-name points to the target repo of the pull
177 request and the vcs-name points to the shadow repo file system path.
178 request and the vcs-name points to the shadow repo file system path.
178 The url-name is always the URL used by the vcs client program.
179 The url-name is always the URL used by the vcs client program.
179
180
180 Example in case of a shadow repo:
181 Example in case of a shadow repo:
181 acl_repo_name = RepoGroup/MyRepo
182 acl_repo_name = RepoGroup/MyRepo
182 url_repo_name = RepoGroup/MyRepo/pull-request/3/repository
183 url_repo_name = RepoGroup/MyRepo/pull-request/3/repository
183 vcs_repo_name = /repo/base/path/RepoGroup/.__shadow_MyRepo_pr-3'
184 vcs_repo_name = /repo/base/path/RepoGroup/.__shadow_MyRepo_pr-3'
184 """
185 """
185 # First we set the repo name from URL for all attributes. This is the
186 # First we set the repo name from URL for all attributes. This is the
186 # default if handling normal (non shadow) repo requests.
187 # default if handling normal (non shadow) repo requests.
187 self.url_repo_name = self._get_repository_name(environ)
188 self.url_repo_name = self._get_repository_name(environ)
188 self.acl_repo_name = self.vcs_repo_name = self.url_repo_name
189 self.acl_repo_name = self.vcs_repo_name = self.url_repo_name
189 self.is_shadow_repo = False
190 self.is_shadow_repo = False
190
191
191 # Check if this is a request to a shadow repository.
192 # Check if this is a request to a shadow repository.
192 match = self.shadow_repo_re.match(self.url_repo_name)
193 match = self.shadow_repo_re.match(self.url_repo_name)
193 if match:
194 if match:
194 match_dict = match.groupdict()
195 match_dict = match.groupdict()
195
196
196 # Build acl repo name from regex match.
197 # Build acl repo name from regex match.
197 acl_repo_name = safe_str('{groups}{target}'.format(
198 acl_repo_name = safe_str('{groups}{target}'.format(
198 groups=match_dict['groups'] or '',
199 groups=match_dict['groups'] or '',
199 target=match_dict['target']))
200 target=match_dict['target']))
200
201
201 # Retrieve pull request instance by ID from regex match.
202 # Retrieve pull request instance by ID from regex match.
202 pull_request = PullRequest.get(match_dict['pr_id'])
203 pull_request = PullRequest.get(match_dict['pr_id'])
203
204
204 # Only proceed if we got a pull request and if acl repo name from
205 # Only proceed if we got a pull request and if acl repo name from
205 # URL equals the target repo name of the pull request.
206 # URL equals the target repo name of the pull request.
206 if pull_request and (acl_repo_name == pull_request.target_repo.repo_name):
207 if pull_request and (acl_repo_name == pull_request.target_repo.repo_name):
207
208
208 # Get file system path to shadow repository.
209 # Get file system path to shadow repository.
209 workspace_id = PullRequestModel()._workspace_id(pull_request)
210 workspace_id = PullRequestModel()._workspace_id(pull_request)
210 vcs_repo_name = pull_request.target_repo.get_shadow_repository_path(workspace_id)
211 vcs_repo_name = pull_request.target_repo.get_shadow_repository_path(workspace_id)
211
212
212 # Store names for later usage.
213 # Store names for later usage.
213 self.vcs_repo_name = vcs_repo_name
214 self.vcs_repo_name = vcs_repo_name
214 self.acl_repo_name = acl_repo_name
215 self.acl_repo_name = acl_repo_name
215 self.is_shadow_repo = True
216 self.is_shadow_repo = True
216
217
217 log.debug('Setting all VCS repository names: %s', {
218 log.debug('Setting all VCS repository names: %s', {
218 'acl_repo_name': self.acl_repo_name,
219 'acl_repo_name': self.acl_repo_name,
219 'url_repo_name': self.url_repo_name,
220 'url_repo_name': self.url_repo_name,
220 'vcs_repo_name': self.vcs_repo_name,
221 'vcs_repo_name': self.vcs_repo_name,
221 })
222 })
222
223
223 @property
224 @property
224 def scm_app(self):
225 def scm_app(self):
225 custom_implementation = self.config['vcs.scm_app_implementation']
226 custom_implementation = self.config['vcs.scm_app_implementation']
226 if custom_implementation == 'http':
227 if custom_implementation == 'http':
227 log.debug('Using HTTP implementation of scm app.')
228 log.debug('Using HTTP implementation of scm app.')
228 scm_app_impl = scm_app_http
229 scm_app_impl = scm_app_http
229 else:
230 else:
230 log.debug('Using custom implementation of scm_app: "{}"'.format(
231 log.debug('Using custom implementation of scm_app: "{}"'.format(
231 custom_implementation))
232 custom_implementation))
232 scm_app_impl = importlib.import_module(custom_implementation)
233 scm_app_impl = importlib.import_module(custom_implementation)
233 return scm_app_impl
234 return scm_app_impl
234
235
235 def _get_by_id(self, repo_name):
236 def _get_by_id(self, repo_name):
236 """
237 """
237 Gets a special pattern _<ID> from clone url and tries to replace it
238 Gets a special pattern _<ID> from clone url and tries to replace it
238 with a repository_name for support of _<ID> non changeable urls
239 with a repository_name for support of _<ID> non changeable urls
239 """
240 """
240
241
241 data = repo_name.split('/')
242 data = repo_name.split('/')
242 if len(data) >= 2:
243 if len(data) >= 2:
243 from rhodecode.model.repo import RepoModel
244 from rhodecode.model.repo import RepoModel
244 by_id_match = RepoModel().get_repo_by_id(repo_name)
245 by_id_match = RepoModel().get_repo_by_id(repo_name)
245 if by_id_match:
246 if by_id_match:
246 data[1] = by_id_match.repo_name
247 data[1] = by_id_match.repo_name
247
248
248 # Because PEP-3333-WSGI uses bytes-tunneled-in-latin-1 as PATH_INFO
249 # Because PEP-3333-WSGI uses bytes-tunneled-in-latin-1 as PATH_INFO
249 # and we use this data
250 # and we use this data
250 maybe_new_path = '/'.join(data)
251 maybe_new_path = '/'.join(data)
251 return safe_bytes(maybe_new_path).decode('latin1')
252 return safe_bytes(maybe_new_path).decode('latin1')
252
253
253 def _invalidate_cache(self, repo_name):
254 def _invalidate_cache(self, repo_name):
254 """
255 """
255 Set's cache for this repository for invalidation on next access
256 Set's cache for this repository for invalidation on next access
256
257
257 :param repo_name: full repo name, also a cache key
258 :param repo_name: full repo name, also a cache key
258 """
259 """
259 ScmModel().mark_for_invalidation(repo_name)
260 ScmModel().mark_for_invalidation(repo_name)
260
261
261 def is_valid_and_existing_repo(self, repo_name, base_path, scm_type):
262 def is_valid_and_existing_repo(self, repo_name, base_path, scm_type):
262 db_repo = Repository.get_by_repo_name(repo_name)
263 db_repo = Repository.get_by_repo_name(repo_name)
263 if not db_repo:
264 if not db_repo:
264 log.debug('Repository `%s` not found inside the database.',
265 log.debug('Repository `%s` not found inside the database.',
265 repo_name)
266 repo_name)
266 return False
267 return False
267
268
268 if db_repo.repo_type != scm_type:
269 if db_repo.repo_type != scm_type:
269 log.warning(
270 log.warning(
270 'Repository `%s` have incorrect scm_type, expected %s got %s',
271 'Repository `%s` have incorrect scm_type, expected %s got %s',
271 repo_name, db_repo.repo_type, scm_type)
272 repo_name, db_repo.repo_type, scm_type)
272 return False
273 return False
273
274
274 config = db_repo._config
275 config = db_repo._config
275 config.set('extensions', 'largefiles', '')
276 config.set('extensions', 'largefiles', '')
276 return is_valid_repo(
277 return is_valid_repo(
277 repo_name, base_path,
278 repo_name, base_path,
278 explicit_scm=scm_type, expect_scm=scm_type, config=config)
279 explicit_scm=scm_type, expect_scm=scm_type, config=config)
279
280
280 def valid_and_active_user(self, user):
281 def valid_and_active_user(self, user):
281 """
282 """
282 Checks if that user is not empty, and if it's actually object it checks
283 Checks if that user is not empty, and if it's actually object it checks
283 if he's active.
284 if he's active.
284
285
285 :param user: user object or None
286 :param user: user object or None
286 :return: boolean
287 :return: boolean
287 """
288 """
288 if user is None:
289 if user is None:
289 return False
290 return False
290
291
291 elif user.active:
292 elif user.active:
292 return True
293 return True
293
294
294 return False
295 return False
295
296
296 @property
297 @property
297 def is_shadow_repo_dir(self):
298 def is_shadow_repo_dir(self):
298 return os.path.isdir(self.vcs_repo_name)
299 return os.path.isdir(self.vcs_repo_name)
299
300
300 def _check_permission(self, action, user, auth_user, repo_name, ip_addr=None,
301 def _check_permission(self, action, user, auth_user, repo_name, ip_addr=None,
301 plugin_id='', plugin_cache_active=False, cache_ttl=0):
302 plugin_id='', plugin_cache_active=False, cache_ttl=0):
302 """
303 """
303 Checks permissions using action (push/pull) user and repository
304 Checks permissions using action (push/pull) user and repository
304 name. If plugin_cache and ttl is set it will use the plugin which
305 name. If plugin_cache and ttl is set it will use the plugin which
305 authenticated the user to store the cached permissions result for N
306 authenticated the user to store the cached permissions result for N
306 amount of seconds as in cache_ttl
307 amount of seconds as in cache_ttl
307
308
308 :param action: push or pull action
309 :param action: push or pull action
309 :param user: user instance
310 :param user: user instance
310 :param repo_name: repository name
311 :param repo_name: repository name
311 """
312 """
312
313
313 log.debug('AUTH_CACHE_TTL for permissions `%s` active: %s (TTL: %s)',
314 log.debug('AUTH_CACHE_TTL for permissions `%s` active: %s (TTL: %s)',
314 plugin_id, plugin_cache_active, cache_ttl)
315 plugin_id, plugin_cache_active, cache_ttl)
315
316
316 user_id = user.user_id
317 user_id = user.user_id
317 cache_namespace_uid = f'cache_user_auth.{rc_cache.PERMISSIONS_CACHE_VER}.{user_id}'
318 cache_namespace_uid = f'cache_user_auth.{rc_cache.PERMISSIONS_CACHE_VER}.{user_id}'
318 region = rc_cache.get_or_create_region('cache_perms', cache_namespace_uid)
319 region = rc_cache.get_or_create_region('cache_perms', cache_namespace_uid)
319
320
320 @region.conditional_cache_on_arguments(namespace=cache_namespace_uid,
321 @region.conditional_cache_on_arguments(namespace=cache_namespace_uid,
321 expiration_time=cache_ttl,
322 expiration_time=cache_ttl,
322 condition=plugin_cache_active)
323 condition=plugin_cache_active)
323 def compute_perm_vcs(
324 def compute_perm_vcs(
324 cache_name, plugin_id, action, user_id, repo_name, ip_addr):
325 cache_name, plugin_id, action, user_id, repo_name, ip_addr):
325
326
326 log.debug('auth: calculating permission access now...')
327 log.debug('auth: calculating permission access now...')
327 # check IP
328 # check IP
328 inherit = user.inherit_default_permissions
329 inherit = user.inherit_default_permissions
329 ip_allowed = AuthUser.check_ip_allowed(
330 ip_allowed = AuthUser.check_ip_allowed(
330 user_id, ip_addr, inherit_from_default=inherit)
331 user_id, ip_addr, inherit_from_default=inherit)
331 if ip_allowed:
332 if ip_allowed:
332 log.info('Access for IP:%s allowed', ip_addr)
333 log.info('Access for IP:%s allowed', ip_addr)
333 else:
334 else:
334 return False
335 return False
335
336
336 if action == 'push':
337 if action == 'push':
337 perms = ('repository.write', 'repository.admin')
338 perms = ('repository.write', 'repository.admin')
338 if not HasPermissionAnyMiddleware(*perms)(auth_user, repo_name):
339 if not HasPermissionAnyMiddleware(*perms)(auth_user, repo_name):
339 return False
340 return False
340
341
341 else:
342 else:
342 # any other action need at least read permission
343 # any other action need at least read permission
343 perms = (
344 perms = (
344 'repository.read', 'repository.write', 'repository.admin')
345 'repository.read', 'repository.write', 'repository.admin')
345 if not HasPermissionAnyMiddleware(*perms)(auth_user, repo_name):
346 if not HasPermissionAnyMiddleware(*perms)(auth_user, repo_name):
346 return False
347 return False
347
348
348 return True
349 return True
349
350
350 start = time.time()
351 start = time.time()
351 log.debug('Running plugin `%s` permissions check', plugin_id)
352 log.debug('Running plugin `%s` permissions check', plugin_id)
352
353
353 # for environ based auth, password can be empty, but then the validation is
354 # for environ based auth, password can be empty, but then the validation is
354 # on the server that fills in the env data needed for authentication
355 # on the server that fills in the env data needed for authentication
355 perm_result = compute_perm_vcs(
356 perm_result = compute_perm_vcs(
356 'vcs_permissions', plugin_id, action, user.user_id, repo_name, ip_addr)
357 'vcs_permissions', plugin_id, action, user.user_id, repo_name, ip_addr)
357
358
358 auth_time = time.time() - start
359 auth_time = time.time() - start
359 log.debug('Permissions for plugin `%s` completed in %.4fs, '
360 log.debug('Permissions for plugin `%s` completed in %.4fs, '
360 'expiration time of fetched cache %.1fs.',
361 'expiration time of fetched cache %.1fs.',
361 plugin_id, auth_time, cache_ttl)
362 plugin_id, auth_time, cache_ttl)
362
363
363 return perm_result
364 return perm_result
364
365
365 def _get_http_scheme(self, environ):
366 def _get_http_scheme(self, environ):
366 try:
367 try:
367 return environ['wsgi.url_scheme']
368 return environ['wsgi.url_scheme']
368 except Exception:
369 except Exception:
369 log.exception('Failed to read http scheme')
370 log.exception('Failed to read http scheme')
370 return 'http'
371 return 'http'
371
372
372 def _check_ssl(self, environ, start_response):
373 def _check_ssl(self, environ, start_response):
373 """
374 """
374 Checks the SSL check flag and returns False if SSL is not present
375 Checks the SSL check flag and returns False if SSL is not present
375 and required True otherwise
376 and required True otherwise
376 """
377 """
377 org_proto = environ['wsgi._org_proto']
378 org_proto = environ['wsgi._org_proto']
378 # check if we have SSL required ! if not it's a bad request !
379 # check if we have SSL required ! if not it's a bad request !
379 require_ssl = str2bool(self.repo_vcs_config.get('web', 'push_ssl'))
380 require_ssl = str2bool(self.repo_vcs_config.get('web', 'push_ssl'))
380 if require_ssl and org_proto == 'http':
381 if require_ssl and org_proto == 'http':
381 log.debug(
382 log.debug(
382 'Bad request: detected protocol is `%s` and '
383 'Bad request: detected protocol is `%s` and '
383 'SSL/HTTPS is required.', org_proto)
384 'SSL/HTTPS is required.', org_proto)
384 return False
385 return False
385 return True
386 return True
386
387
387 def _get_default_cache_ttl(self):
388 def _get_default_cache_ttl(self):
388 # take AUTH_CACHE_TTL from the `rhodecode` auth plugin
389 # take AUTH_CACHE_TTL from the `rhodecode` auth plugin
389 plugin = loadplugin('egg:rhodecode-enterprise-ce#rhodecode')
390 plugin = loadplugin('egg:rhodecode-enterprise-ce#rhodecode')
390 plugin_settings = plugin.get_settings()
391 plugin_settings = plugin.get_settings()
391 plugin_cache_active, cache_ttl = plugin.get_ttl_cache(
392 plugin_cache_active, cache_ttl = plugin.get_ttl_cache(
392 plugin_settings) or (False, 0)
393 plugin_settings) or (False, 0)
393 return plugin_cache_active, cache_ttl
394 return plugin_cache_active, cache_ttl
394
395
395 def __call__(self, environ, start_response):
396 def __call__(self, environ, start_response):
396 try:
397 try:
397 return self._handle_request(environ, start_response)
398 return self._handle_request(environ, start_response)
398 except Exception:
399 except Exception:
399 log.exception("Exception while handling request")
400 log.exception("Exception while handling request")
400 appenlight.track_exception(environ)
401 appenlight.track_exception(environ)
401 return HTTPInternalServerError()(environ, start_response)
402 return HTTPInternalServerError()(environ, start_response)
402 finally:
403 finally:
403 meta.Session.remove()
404 meta.Session.remove()
404
405
405 def _handle_request(self, environ, start_response):
406 def _handle_request(self, environ, start_response):
406 if not self._check_ssl(environ, start_response):
407 if not self._check_ssl(environ, start_response):
407 reason = ('SSL required, while RhodeCode was unable '
408 reason = ('SSL required, while RhodeCode was unable '
408 'to detect this as SSL request')
409 'to detect this as SSL request')
409 log.debug('User not allowed to proceed, %s', reason)
410 log.debug('User not allowed to proceed, %s', reason)
410 return HTTPNotAcceptable(reason)(environ, start_response)
411 return HTTPNotAcceptable(reason)(environ, start_response)
411
412
412 if not self.url_repo_name:
413 if not self.url_repo_name:
413 log.warning('Repository name is empty: %s', self.url_repo_name)
414 log.warning('Repository name is empty: %s', self.url_repo_name)
414 # failed to get repo name, we fail now
415 # failed to get repo name, we fail now
415 return HTTPNotFound()(environ, start_response)
416 return HTTPNotFound()(environ, start_response)
416 log.debug('Extracted repo name is %s', self.url_repo_name)
417 log.debug('Extracted repo name is %s', self.url_repo_name)
417
418
418 ip_addr = get_ip_addr(environ)
419 ip_addr = get_ip_addr(environ)
419 user_agent = get_user_agent(environ)
420 user_agent = get_user_agent(environ)
420 username = None
421 username = None
421
422
422 # skip passing error to error controller
423 # skip passing error to error controller
423 environ['pylons.status_code_redirect'] = True
424 environ['pylons.status_code_redirect'] = True
424
425
425 # ======================================================================
426 # ======================================================================
426 # GET ACTION PULL or PUSH
427 # GET ACTION PULL or PUSH
427 # ======================================================================
428 # ======================================================================
428 action = self._get_action(environ)
429 action = self._get_action(environ)
429
430
430 # ======================================================================
431 # ======================================================================
431 # Check if this is a request to a shadow repository of a pull request.
432 # Check if this is a request to a shadow repository of a pull request.
432 # In this case only pull action is allowed.
433 # In this case only pull action is allowed.
433 # ======================================================================
434 # ======================================================================
434 if self.is_shadow_repo and action != 'pull':
435 if self.is_shadow_repo and action != 'pull':
435 reason = 'Only pull action is allowed for shadow repositories.'
436 reason = 'Only pull action is allowed for shadow repositories.'
436 log.debug('User not allowed to proceed, %s', reason)
437 log.debug('User not allowed to proceed, %s', reason)
437 return HTTPNotAcceptable(reason)(environ, start_response)
438 return HTTPNotAcceptable(reason)(environ, start_response)
438
439
439 # Check if the shadow repo actually exists, in case someone refers
440 # Check if the shadow repo actually exists, in case someone refers
440 # to it, and it has been deleted because of successful merge.
441 # to it, and it has been deleted because of successful merge.
441 if self.is_shadow_repo and not self.is_shadow_repo_dir:
442 if self.is_shadow_repo and not self.is_shadow_repo_dir:
442 log.debug(
443 log.debug(
443 'Shadow repo detected, and shadow repo dir `%s` is missing',
444 'Shadow repo detected, and shadow repo dir `%s` is missing',
444 self.is_shadow_repo_dir)
445 self.is_shadow_repo_dir)
445 return HTTPNotFound()(environ, start_response)
446 return HTTPNotFound()(environ, start_response)
446
447
447 # ======================================================================
448 # ======================================================================
448 # CHECK ANONYMOUS PERMISSION
449 # CHECK ANONYMOUS PERMISSION
449 # ======================================================================
450 # ======================================================================
450 detect_force_push = False
451 detect_force_push = False
451 check_branch_perms = False
452 check_branch_perms = False
452 if action in ['pull', 'push']:
453 if action in ['pull', 'push']:
453 user_obj = anonymous_user = User.get_default_user()
454 user_obj = anonymous_user = User.get_default_user()
454 auth_user = user_obj.AuthUser()
455 auth_user = user_obj.AuthUser()
455 username = anonymous_user.username
456 username = anonymous_user.username
456 if anonymous_user.active:
457 if anonymous_user.active:
457 plugin_cache_active, cache_ttl = self._get_default_cache_ttl()
458 plugin_cache_active, cache_ttl = self._get_default_cache_ttl()
458 # ONLY check permissions if the user is activated
459 # ONLY check permissions if the user is activated
459 anonymous_perm = self._check_permission(
460 anonymous_perm = self._check_permission(
460 action, anonymous_user, auth_user, self.acl_repo_name, ip_addr,
461 action, anonymous_user, auth_user, self.acl_repo_name, ip_addr,
461 plugin_id='anonymous_access',
462 plugin_id='anonymous_access',
462 plugin_cache_active=plugin_cache_active,
463 plugin_cache_active=plugin_cache_active,
463 cache_ttl=cache_ttl,
464 cache_ttl=cache_ttl,
464 )
465 )
465 else:
466 else:
466 anonymous_perm = False
467 anonymous_perm = False
467
468
468 if not anonymous_user.active or not anonymous_perm:
469 if not anonymous_user.active or not anonymous_perm:
469 if not anonymous_user.active:
470 if not anonymous_user.active:
470 log.debug('Anonymous access is disabled, running '
471 log.debug('Anonymous access is disabled, running '
471 'authentication')
472 'authentication')
472
473
473 if not anonymous_perm:
474 if not anonymous_perm:
474 log.debug('Not enough credentials to access repo: `%s` '
475 log.debug('Not enough credentials to access repo: `%s` '
475 'repository as anonymous user', self.acl_repo_name)
476 'repository as anonymous user', self.acl_repo_name)
476
477
477
478
478 username = None
479 username = None
479 # ==============================================================
480 # ==============================================================
480 # DEFAULT PERM FAILED OR ANONYMOUS ACCESS IS DISABLED SO WE
481 # DEFAULT PERM FAILED OR ANONYMOUS ACCESS IS DISABLED SO WE
481 # NEED TO AUTHENTICATE AND ASK FOR AUTH USER PERMISSIONS
482 # NEED TO AUTHENTICATE AND ASK FOR AUTH USER PERMISSIONS
482 # ==============================================================
483 # ==============================================================
483
484
484 # try to auth based on environ, container auth methods
485 # try to auth based on environ, container auth methods
485 log.debug('Running PRE-AUTH for container|headers based authentication')
486 log.debug('Running PRE-AUTH for container|headers based authentication')
486
487
487 # headers auth, by just reading special headers and bypass the auth with user/passwd
488 # headers auth, by just reading special headers and bypass the auth with user/passwd
488 pre_auth = authenticate(
489 pre_auth = authenticate(
489 '', '', environ, VCS_TYPE, registry=self.registry,
490 '', '', environ, VCS_TYPE, registry=self.registry,
490 acl_repo_name=self.acl_repo_name)
491 acl_repo_name=self.acl_repo_name)
491
492
492 if pre_auth and pre_auth.get('username'):
493 if pre_auth and pre_auth.get('username'):
493 username = pre_auth['username']
494 username = pre_auth['username']
494 log.debug('PRE-AUTH got `%s` as username', username)
495 log.debug('PRE-AUTH got `%s` as username', username)
495 if pre_auth:
496 if pre_auth:
496 log.debug('PRE-AUTH successful from %s',
497 log.debug('PRE-AUTH successful from %s',
497 pre_auth.get('auth_data', {}).get('_plugin'))
498 pre_auth.get('auth_data', {}).get('_plugin'))
498
499
499 # If not authenticated by the container, running basic auth
500 # If not authenticated by the container, running basic auth
500 # before inject the calling repo_name for special scope checks
501 # before inject the calling repo_name for special scope checks
501 self.authenticate.acl_repo_name = self.acl_repo_name
502 self.authenticate.acl_repo_name = self.acl_repo_name
502
503
503 plugin_cache_active, cache_ttl = False, 0
504 plugin_cache_active, cache_ttl = False, 0
504 plugin = None
505 plugin = None
505
506
506 # regular auth chain
507 # regular auth chain
507 if not username:
508 if not username:
508 self.authenticate.realm = self.authenticate.get_rc_realm()
509 self.authenticate.realm = self.authenticate.get_rc_realm()
509
510
510 try:
511 try:
511 auth_result = self.authenticate(environ)
512 auth_result = self.authenticate(environ)
512 except (UserCreationError, NotAllowedToCreateUserError) as e:
513 except (UserCreationError, NotAllowedToCreateUserError) as e:
513 log.error(e)
514 log.error(e)
514 reason = safe_str(e)
515 reason = safe_str(e)
515 return HTTPNotAcceptable(reason)(environ, start_response)
516 return HTTPNotAcceptable(reason)(environ, start_response)
516
517
517 if isinstance(auth_result, dict):
518 if isinstance(auth_result, dict):
518 AUTH_TYPE.update(environ, 'basic')
519 AUTH_TYPE.update(environ, 'basic')
519 REMOTE_USER.update(environ, auth_result['username'])
520 REMOTE_USER.update(environ, auth_result['username'])
520 username = auth_result['username']
521 username = auth_result['username']
521 plugin = auth_result.get('auth_data', {}).get('_plugin')
522 plugin = auth_result.get('auth_data', {}).get('_plugin')
522 log.info(
523 log.info(
523 'MAIN-AUTH successful for user `%s` from %s plugin',
524 'MAIN-AUTH successful for user `%s` from %s plugin',
524 username, plugin)
525 username, plugin)
525
526
526 plugin_cache_active, cache_ttl = auth_result.get(
527 plugin_cache_active, cache_ttl = auth_result.get(
527 'auth_data', {}).get('_ttl_cache') or (False, 0)
528 'auth_data', {}).get('_ttl_cache') or (False, 0)
528 else:
529 else:
529 return auth_result.wsgi_application(environ, start_response)
530 return auth_result.wsgi_application(environ, start_response)
530
531
531 # ==============================================================
532 # ==============================================================
532 # CHECK PERMISSIONS FOR THIS REQUEST USING GIVEN USERNAME
533 # CHECK PERMISSIONS FOR THIS REQUEST USING GIVEN USERNAME
533 # ==============================================================
534 # ==============================================================
534 user = User.get_by_username(username)
535 user = User.get_by_username(username)
535 if not self.valid_and_active_user(user):
536 if not self.valid_and_active_user(user):
536 return HTTPForbidden()(environ, start_response)
537 return HTTPForbidden()(environ, start_response)
537 username = user.username
538 username = user.username
538 user_id = user.user_id
539 user_id = user.user_id
539
540
540 # check user attributes for password change flag
541 # check user attributes for password change flag
541 user_obj = user
542 user_obj = user
542 auth_user = user_obj.AuthUser()
543 auth_user = user_obj.AuthUser()
543 if user_obj and user_obj.username != User.DEFAULT_USER and \
544 if user_obj and user_obj.username != User.DEFAULT_USER and \
544 user_obj.user_data.get('force_password_change'):
545 user_obj.user_data.get('force_password_change'):
545 reason = 'password change required'
546 reason = 'password change required'
546 log.debug('User not allowed to authenticate, %s', reason)
547 log.debug('User not allowed to authenticate, %s', reason)
547 return HTTPNotAcceptable(reason)(environ, start_response)
548 return HTTPNotAcceptable(reason)(environ, start_response)
548
549
549 # check permissions for this repository
550 # check permissions for this repository
550 perm = self._check_permission(
551 perm = self._check_permission(
551 action, user, auth_user, self.acl_repo_name, ip_addr,
552 action, user, auth_user, self.acl_repo_name, ip_addr,
552 plugin, plugin_cache_active, cache_ttl)
553 plugin, plugin_cache_active, cache_ttl)
553 if not perm:
554 if not perm:
554 return HTTPForbidden()(environ, start_response)
555 return HTTPForbidden()(environ, start_response)
555 environ['rc_auth_user_id'] = str(user_id)
556 environ['rc_auth_user_id'] = str(user_id)
556
557
557 if action == 'push':
558 if action == 'push':
558 perms = auth_user.get_branch_permissions(self.acl_repo_name)
559 perms = auth_user.get_branch_permissions(self.acl_repo_name)
559 if perms:
560 if perms:
560 check_branch_perms = True
561 check_branch_perms = True
561 detect_force_push = True
562 detect_force_push = True
562
563
563 # extras are injected into UI object and later available
564 # extras are injected into UI object and later available
564 # in hooks executed by RhodeCode
565 # in hooks executed by RhodeCode
565 check_locking = _should_check_locking(environ.get('QUERY_STRING'))
566 check_locking = _should_check_locking(environ.get('QUERY_STRING'))
566
567
567 extras = vcs_operation_context(
568 extras = vcs_operation_context(
568 environ, repo_name=self.acl_repo_name, username=username,
569 environ, repo_name=self.acl_repo_name, username=username,
569 action=action, scm=self.SCM, check_locking=check_locking,
570 action=action, scm=self.SCM, check_locking=check_locking,
570 is_shadow_repo=self.is_shadow_repo, check_branch_perms=check_branch_perms,
571 is_shadow_repo=self.is_shadow_repo, check_branch_perms=check_branch_perms,
571 detect_force_push=detect_force_push
572 detect_force_push=detect_force_push
572 )
573 )
573
574
574 # ======================================================================
575 # ======================================================================
575 # REQUEST HANDLING
576 # REQUEST HANDLING
576 # ======================================================================
577 # ======================================================================
577 repo_path = os.path.join(
578 repo_path = os.path.join(
578 safe_str(self.base_path), safe_str(self.vcs_repo_name))
579 safe_str(self.base_path), safe_str(self.vcs_repo_name))
579 log.debug('Repository path is %s', repo_path)
580 log.debug('Repository path is %s', repo_path)
580
581
581 fix_PATH()
582 fix_PATH()
582
583
583 log.info(
584 log.info(
584 '%s action on %s repo "%s" by "%s" from %s %s',
585 '%s action on %s repo "%s" by "%s" from %s %s',
585 action, self.SCM, safe_str(self.url_repo_name),
586 action, self.SCM, safe_str(self.url_repo_name),
586 safe_str(username), ip_addr, user_agent)
587 safe_str(username), ip_addr, user_agent)
587
588
588 return self._generate_vcs_response(
589 return self._generate_vcs_response(
589 environ, start_response, repo_path, extras, action)
590 environ, start_response, repo_path, extras, action)
590
591
591 @initialize_generator
592 @initialize_generator
592 def _generate_vcs_response(
593 def _generate_vcs_response(
593 self, environ, start_response, repo_path, extras, action):
594 self, environ, start_response, repo_path, extras, action):
594 """
595 """
595 Returns a generator for the response content.
596 Returns a generator for the response content.
596
597
597 This method is implemented as a generator, so that it can trigger
598 This method is implemented as a generator, so that it can trigger
598 the cache validation after all content sent back to the client. It
599 the cache validation after all content sent back to the client. It
599 also handles the locking exceptions which will be triggered when
600 also handles the locking exceptions which will be triggered when
600 the first chunk is produced by the underlying WSGI application.
601 the first chunk is produced by the underlying WSGI application.
601 """
602 """
602
603
603 txn_id = ''
604 txn_id = ''
604 if 'CONTENT_LENGTH' in environ and environ['REQUEST_METHOD'] == 'MERGE':
605 if 'CONTENT_LENGTH' in environ and environ['REQUEST_METHOD'] == 'MERGE':
605 # case for SVN, we want to re-use the callback daemon port
606 # case for SVN, we want to re-use the callback daemon port
606 # so we use the txn_id, for this we peek the body, and still save
607 # so we use the txn_id, for this we peek the body, and still save
607 # it as wsgi.input
608 # it as wsgi.input
608
609
609 stream = environ['wsgi.input']
610 stream = environ['wsgi.input']
610
611
611 if isinstance(stream, io.BytesIO):
612 if isinstance(stream, io.BytesIO):
612 data: str = safe_str(stream.getvalue())
613 data: bytes = stream.getvalue()
613 elif hasattr(stream, 'buf'): # most likely gunicorn.http.body.Body
614 elif hasattr(stream, 'buf'): # most likely gunicorn.http.body.Body
614 data: str = safe_str(stream.buf.getvalue())
615 data: bytes = stream.buf.getvalue()
615 else:
616 else:
616 # fallback to the crudest way, copy the iterator
617 # fallback to the crudest way, copy the iterator
617 data = safe_str(stream.read())
618 data = safe_bytes(stream.read())
618 environ['wsgi.input'] = io.BytesIO(safe_bytes(data))
619 environ['wsgi.input'] = io.BytesIO(data)
619
620
620 txn_id = extract_svn_txn_id(self.acl_repo_name, data)
621 txn_id = extract_svn_txn_id(self.acl_repo_name, data)
621
622
622 callback_daemon, extras = self._prepare_callback_daemon(
623 callback_daemon, extras = self._prepare_callback_daemon(
623 extras, environ, action, txn_id=txn_id)
624 extras, environ, action, txn_id=txn_id)
624 log.debug('HOOKS extras is %s', extras)
625 log.debug('HOOKS extras is %s', extras)
625
626
626 http_scheme = self._get_http_scheme(environ)
627 http_scheme = self._get_http_scheme(environ)
627
628
628 config = self._create_config(extras, self.acl_repo_name, scheme=http_scheme)
629 config = self._create_config(extras, self.acl_repo_name, scheme=http_scheme)
629 app = self._create_wsgi_app(repo_path, self.url_repo_name, config)
630 app = self._create_wsgi_app(repo_path, self.url_repo_name, config)
630 with callback_daemon:
631 with callback_daemon:
631 app.rc_extras = extras
632 app.rc_extras = extras
632
633
633 try:
634 try:
634 response = app(environ, start_response)
635 response = app(environ, start_response)
635 finally:
636 finally:
636 # This statement works together with the decorator
637 # This statement works together with the decorator
637 # "initialize_generator" above. The decorator ensures that
638 # "initialize_generator" above. The decorator ensures that
638 # we hit the first yield statement before the generator is
639 # we hit the first yield statement before the generator is
639 # returned back to the WSGI server. This is needed to
640 # returned back to the WSGI server. This is needed to
640 # ensure that the call to "app" above triggers the
641 # ensure that the call to "app" above triggers the
641 # needed callback to "start_response" before the
642 # needed callback to "start_response" before the
642 # generator is actually used.
643 # generator is actually used.
643 yield "__init__"
644 yield "__init__"
644
645
645 # iter content
646 # iter content
646 for chunk in response:
647 for chunk in response:
647 yield chunk
648 yield chunk
648
649
649 try:
650 try:
650 # invalidate cache on push
651 # invalidate cache on push
651 if action == 'push':
652 if action == 'push':
652 self._invalidate_cache(self.url_repo_name)
653 self._invalidate_cache(self.url_repo_name)
653 finally:
654 finally:
654 meta.Session.remove()
655 meta.Session.remove()
655
656
656 def _get_repository_name(self, environ):
657 def _get_repository_name(self, environ):
657 """Get repository name out of the environmnent
658 """Get repository name out of the environmnent
658
659
659 :param environ: WSGI environment
660 :param environ: WSGI environment
660 """
661 """
661 raise NotImplementedError()
662 raise NotImplementedError()
662
663
663 def _get_action(self, environ):
664 def _get_action(self, environ):
664 """Map request commands into a pull or push command.
665 """Map request commands into a pull or push command.
665
666
666 :param environ: WSGI environment
667 :param environ: WSGI environment
667 """
668 """
668 raise NotImplementedError()
669 raise NotImplementedError()
669
670
670 def _create_wsgi_app(self, repo_path, repo_name, config):
671 def _create_wsgi_app(self, repo_path, repo_name, config):
671 """Return the WSGI app that will finally handle the request."""
672 """Return the WSGI app that will finally handle the request."""
672 raise NotImplementedError()
673 raise NotImplementedError()
673
674
674 def _create_config(self, extras, repo_name, scheme='http'):
675 def _create_config(self, extras, repo_name, scheme='http'):
675 """Create a safe config representation."""
676 """Create a safe config representation."""
676 raise NotImplementedError()
677 raise NotImplementedError()
677
678
678 def _should_use_callback_daemon(self, extras, environ, action):
679 def _should_use_callback_daemon(self, extras, environ, action):
679 if extras.get('is_shadow_repo'):
680 if extras.get('is_shadow_repo'):
680 # we don't want to execute hooks, and callback daemon for shadow repos
681 # we don't want to execute hooks, and callback daemon for shadow repos
681 return False
682 return False
682 return True
683 return True
683
684
684 def _prepare_callback_daemon(self, extras, environ, action, txn_id=None):
685 def _prepare_callback_daemon(self, extras, environ, action, txn_id=None):
685 direct_calls = vcs_settings.HOOKS_DIRECT_CALLS
686 direct_calls = vcs_settings.HOOKS_DIRECT_CALLS
686 if not self._should_use_callback_daemon(extras, environ, action):
687 if not self._should_use_callback_daemon(extras, environ, action):
687 # disable callback daemon for actions that don't require it
688 # disable callback daemon for actions that don't require it
688 direct_calls = True
689 direct_calls = True
689
690
690 return prepare_callback_daemon(
691 return prepare_callback_daemon(
691 extras, protocol=vcs_settings.HOOKS_PROTOCOL,
692 extras, protocol=vcs_settings.HOOKS_PROTOCOL,
692 host=vcs_settings.HOOKS_HOST, use_direct_calls=direct_calls, txn_id=txn_id)
693 host=vcs_settings.HOOKS_HOST, use_direct_calls=direct_calls, txn_id=txn_id)
693
694
694
695
695 def _should_check_locking(query_string):
696 def _should_check_locking(query_string):
696 # this is kind of hacky, but due to how mercurial handles client-server
697 # this is kind of hacky, but due to how mercurial handles client-server
697 # server see all operation on commit; bookmarks, phases and
698 # server see all operation on commit; bookmarks, phases and
698 # obsolescence marker in different transaction, we don't want to check
699 # obsolescence marker in different transaction, we don't want to check
699 # locking on those
700 # locking on those
700 return query_string not in ['cmd=listkeys']
701 return query_string not in ['cmd=listkeys']
@@ -1,298 +1,302 b''
1
1
2 # Copyright (C) 2010-2023 RhodeCode GmbH
2 # Copyright (C) 2010-2023 RhodeCode GmbH
3 #
3 #
4 # This program is free software: you can redistribute it and/or modify
4 # This program is free software: you can redistribute it and/or modify
5 # it under the terms of the GNU Affero General Public License, version 3
5 # it under the terms of the GNU Affero General Public License, version 3
6 # (only), as published by the Free Software Foundation.
6 # (only), as published by the Free Software Foundation.
7 #
7 #
8 # This program is distributed in the hope that it will be useful,
8 # This program is distributed in the hope that it will be useful,
9 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # GNU General Public License for more details.
11 # GNU General Public License for more details.
12 #
12 #
13 # You should have received a copy of the GNU Affero General Public License
13 # You should have received a copy of the GNU Affero General Public License
14 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 #
15 #
16 # This program is dual-licensed. If you wish to learn more about the
16 # This program is dual-licensed. If you wish to learn more about the
17 # RhodeCode Enterprise Edition, including its added features, Support services,
17 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # and proprietary license terms, please see https://rhodecode.com/licenses/
18 # and proprietary license terms, please see https://rhodecode.com/licenses/
19
19
20 import gzip
20 import gzip
21 import shutil
21 import shutil
22 import logging
22 import logging
23 import tempfile
23 import tempfile
24 import urllib.parse
24 import urllib.parse
25
25
26 from webob.exc import HTTPNotFound
26 from webob.exc import HTTPNotFound
27
27
28 import rhodecode
28 import rhodecode
29 from rhodecode.lib.middleware.utils import get_path_info
29 from rhodecode.lib.middleware.utils import get_path_info
30 from rhodecode.lib.middleware.appenlight import wrap_in_appenlight_if_enabled
30 from rhodecode.lib.middleware.appenlight import wrap_in_appenlight_if_enabled
31 from rhodecode.lib.middleware.simplegit import SimpleGit, GIT_PROTO_PAT
31 from rhodecode.lib.middleware.simplegit import SimpleGit, GIT_PROTO_PAT
32 from rhodecode.lib.middleware.simplehg import SimpleHg
32 from rhodecode.lib.middleware.simplehg import SimpleHg
33 from rhodecode.lib.middleware.simplesvn import SimpleSvn
33 from rhodecode.lib.middleware.simplesvn import SimpleSvn
34 from rhodecode.model.settings import VcsSettingsModel
34 from rhodecode.model.settings import VcsSettingsModel
35
35
36
36
37 log = logging.getLogger(__name__)
37 log = logging.getLogger(__name__)
38
38
39 VCS_TYPE_KEY = '_rc_vcs_type'
39 VCS_TYPE_KEY = '_rc_vcs_type'
40 VCS_TYPE_SKIP = '_rc_vcs_skip'
40 VCS_TYPE_SKIP = '_rc_vcs_skip'
41
41
42
42
43 def is_git(environ):
43 def is_git(environ):
44 """
44 """
45 Returns True if requests should be handled by GIT wsgi middleware
45 Returns True if requests should be handled by GIT wsgi middleware
46 """
46 """
47 path_info = get_path_info(environ)
47 path_info = get_path_info(environ)
48 is_git_path = GIT_PROTO_PAT.match(path_info)
48 is_git_path = GIT_PROTO_PAT.match(path_info)
49 log.debug(
49 log.debug(
50 'request path: `%s` detected as GIT PROTOCOL %s', path_info,
50 'request path: `%s` detected as GIT PROTOCOL %s', path_info,
51 is_git_path is not None)
51 is_git_path is not None)
52
52
53 return is_git_path
53 return is_git_path
54
54
55
55
56 def is_hg(environ):
56 def is_hg(environ):
57 """
57 """
58 Returns True if requests target is mercurial server - header
58 Returns True if requests target is mercurial server - header
59 ``HTTP_ACCEPT`` of such request would start with ``application/mercurial``.
59 ``HTTP_ACCEPT`` of such request would start with ``application/mercurial``.
60 """
60 """
61 is_hg_path = False
61 is_hg_path = False
62
62
63 http_accept = environ.get('HTTP_ACCEPT')
63 http_accept = environ.get('HTTP_ACCEPT')
64
64
65 if http_accept and http_accept.startswith('application/mercurial'):
65 if http_accept and http_accept.startswith('application/mercurial'):
66 query = urllib.parse.parse_qs(environ['QUERY_STRING'])
66 query = urllib.parse.parse_qs(environ['QUERY_STRING'])
67 if 'cmd' in query:
67 if 'cmd' in query:
68 is_hg_path = True
68 is_hg_path = True
69
69
70 path_info = get_path_info(environ)
70 path_info = get_path_info(environ)
71 log.debug(
71 log.debug(
72 'request path: `%s` detected as HG PROTOCOL %s', path_info,
72 'request path: `%s` detected as HG PROTOCOL %s', path_info,
73 is_hg_path)
73 is_hg_path)
74
74
75 return is_hg_path
75 return is_hg_path
76
76
77
77
78 def is_svn(environ):
78 def is_svn(environ):
79 """
79 """
80 Returns True if requests target is Subversion server
80 Returns True if requests target is Subversion server
81 """
81 """
82
82
83 http_dav = environ.get('HTTP_DAV', '')
83 http_dav = environ.get('HTTP_DAV', '')
84 magic_path_segment = rhodecode.CONFIG.get(
84 magic_path_segment = rhodecode.CONFIG.get(
85 'rhodecode_subversion_magic_path', '/!svn')
85 'rhodecode_subversion_magic_path', '/!svn')
86 path_info = get_path_info(environ)
86 path_info = get_path_info(environ)
87 req_method = environ['REQUEST_METHOD']
88
87 is_svn_path = (
89 is_svn_path = (
88 'subversion' in http_dav or
90 'subversion' in http_dav or
89 magic_path_segment in path_info
91 magic_path_segment in path_info
90 or environ['REQUEST_METHOD'] in ['PROPFIND', 'PROPPATCH']
92 or req_method in ['PROPFIND', 'PROPPATCH', 'HEAD']
91 )
93 )
92 log.debug(
94 log.debug(
93 'request path: `%s` detected as SVN PROTOCOL %s', path_info,
95 'request path: `%s` detected as SVN PROTOCOL %s', path_info,
94 is_svn_path)
96 is_svn_path)
95
97
96 return is_svn_path
98 return is_svn_path
97
99
98
100
99 class GunzipMiddleware(object):
101 class GunzipMiddleware(object):
100 """
102 """
101 WSGI middleware that unzips gzip-encoded requests before
103 WSGI middleware that unzips gzip-encoded requests before
102 passing on to the underlying application.
104 passing on to the underlying application.
103 """
105 """
104
106
105 def __init__(self, application):
107 def __init__(self, application):
106 self.app = application
108 self.app = application
107
109
108 def __call__(self, environ, start_response):
110 def __call__(self, environ, start_response):
109 accepts_encoding_header = environ.get('HTTP_CONTENT_ENCODING', b'')
111 accepts_encoding_header = environ.get('HTTP_CONTENT_ENCODING', b'')
110
112
111 if b'gzip' in accepts_encoding_header:
113 if b'gzip' in accepts_encoding_header:
112 log.debug('gzip detected, now running gunzip wrapper')
114 log.debug('gzip detected, now running gunzip wrapper')
113 wsgi_input = environ['wsgi.input']
115 wsgi_input = environ['wsgi.input']
114
116
115 if not hasattr(environ['wsgi.input'], 'seek'):
117 if not hasattr(environ['wsgi.input'], 'seek'):
116 # The gzip implementation in the standard library of Python 2.x
118 # The gzip implementation in the standard library of Python 2.x
117 # requires the '.seek()' and '.tell()' methods to be available
119 # requires the '.seek()' and '.tell()' methods to be available
118 # on the input stream. Read the data into a temporary file to
120 # on the input stream. Read the data into a temporary file to
119 # work around this limitation.
121 # work around this limitation.
120
122
121 wsgi_input = tempfile.SpooledTemporaryFile(64 * 1024 * 1024)
123 wsgi_input = tempfile.SpooledTemporaryFile(64 * 1024 * 1024)
122 shutil.copyfileobj(environ['wsgi.input'], wsgi_input)
124 shutil.copyfileobj(environ['wsgi.input'], wsgi_input)
123 wsgi_input.seek(0)
125 wsgi_input.seek(0)
124
126
125 environ['wsgi.input'] = gzip.GzipFile(fileobj=wsgi_input, mode='r')
127 environ['wsgi.input'] = gzip.GzipFile(fileobj=wsgi_input, mode='r')
126 # since we "Ungzipped" the content we say now it's no longer gzip
128 # since we "Ungzipped" the content we say now it's no longer gzip
127 # content encoding
129 # content encoding
128 del environ['HTTP_CONTENT_ENCODING']
130 del environ['HTTP_CONTENT_ENCODING']
129
131
130 # content length has changes ? or i'm not sure
132 # content length has changes ? or i'm not sure
131 if 'CONTENT_LENGTH' in environ:
133 if 'CONTENT_LENGTH' in environ:
132 del environ['CONTENT_LENGTH']
134 del environ['CONTENT_LENGTH']
133 else:
135 else:
134 log.debug('content not gzipped, gzipMiddleware passing '
136 log.debug('content not gzipped, gzipMiddleware passing '
135 'request further')
137 'request further')
136 return self.app(environ, start_response)
138 return self.app(environ, start_response)
137
139
138
140
139 def is_vcs_call(environ):
141 def is_vcs_call(environ):
140 if VCS_TYPE_KEY in environ:
142 if VCS_TYPE_KEY in environ:
141 raw_type = environ[VCS_TYPE_KEY]
143 raw_type = environ[VCS_TYPE_KEY]
142 return raw_type and raw_type != VCS_TYPE_SKIP
144 return raw_type and raw_type != VCS_TYPE_SKIP
143 return False
145 return False
144
146
145
147
146 def detect_vcs_request(environ, backends):
148 def detect_vcs_request(environ, backends):
147 checks = {
149 checks = {
148 'hg': (is_hg, SimpleHg),
150 'hg': (is_hg, SimpleHg),
149 'git': (is_git, SimpleGit),
151 'git': (is_git, SimpleGit),
150 'svn': (is_svn, SimpleSvn),
152 'svn': (is_svn, SimpleSvn),
151 }
153 }
152 handler = None
154 handler = None
153 # List of path views first chunk we don't do any checks
155 # List of path views first chunk we don't do any checks
154 white_list = [
156 white_list = [
155 # favicon often requested by browsers
157 # favicon often requested by browsers
156 'favicon.ico',
158 'favicon.ico',
157
159
158 # e.g /_file_store/download
160 # e.g /_file_store/download
159 '_file_store++',
161 '_file_store++',
160
162
161 # login
163 # login
162 "_admin/login",
164 "_admin/login",
163
165
164 # _admin/api is safe too
166 # _admin/api is safe too
165 '_admin/api',
167 '_admin/api',
166
168
167 # _admin/gist is safe too
169 # _admin/gist is safe too
168 '_admin/gists++',
170 '_admin/gists++',
169
171
170 # _admin/my_account is safe too
172 # _admin/my_account is safe too
171 '_admin/my_account++',
173 '_admin/my_account++',
172
174
173 # static files no detection
175 # static files no detection
174 '_static++',
176 '_static++',
175
177
176 # debug-toolbar
178 # debug-toolbar
177 '_debug_toolbar++',
179 '_debug_toolbar++',
178
180
179 # skip ops ping, status
181 # skip ops ping, status
180 '_admin/ops/ping',
182 '_admin/ops/ping',
181 '_admin/ops/status',
183 '_admin/ops/status',
182
184
183 # full channelstream connect should be VCS skipped
185 # full channelstream connect should be VCS skipped
184 '_admin/channelstream/connect',
186 '_admin/channelstream/connect',
185
187
186 '++/repo_creating_check'
188 '++/repo_creating_check'
187 ]
189 ]
188 path_info = get_path_info(environ)
190 path_info = get_path_info(environ)
189 path_url = path_info.lstrip('/')
191 path_url = path_info.lstrip('/')
192 req_method = environ.get('REQUEST_METHOD')
190
193
191 for item in white_list:
194 for item in white_list:
192 if item.endswith('++') and path_url.startswith(item[:-2]):
195 if item.endswith('++') and path_url.startswith(item[:-2]):
193 log.debug('path `%s` in whitelist (match:%s), skipping...', path_url, item)
196 log.debug('path `%s` in whitelist (match:%s), skipping...', path_url, item)
194 return handler
197 return handler
195 if item.startswith('++') and path_url.endswith(item[2:]):
198 if item.startswith('++') and path_url.endswith(item[2:]):
196 log.debug('path `%s` in whitelist (match:%s), skipping...', path_url, item)
199 log.debug('path `%s` in whitelist (match:%s), skipping...', path_url, item)
197 return handler
200 return handler
198 if item == path_url:
201 if item == path_url:
199 log.debug('path `%s` in whitelist (match:%s), skipping...', path_url, item)
202 log.debug('path `%s` in whitelist (match:%s), skipping...', path_url, item)
200 return handler
203 return handler
201
204
202 if VCS_TYPE_KEY in environ:
205 if VCS_TYPE_KEY in environ:
203 raw_type = environ[VCS_TYPE_KEY]
206 raw_type = environ[VCS_TYPE_KEY]
204 if raw_type == VCS_TYPE_SKIP:
207 if raw_type == VCS_TYPE_SKIP:
205 log.debug('got `skip` marker for vcs detection, skipping...')
208 log.debug('got `skip` marker for vcs detection, skipping...')
206 return handler
209 return handler
207
210
208 _check, handler = checks.get(raw_type) or [None, None]
211 _check, handler = checks.get(raw_type) or [None, None]
209 if handler:
212 if handler:
210 log.debug('got handler:%s from environ', handler)
213 log.debug('got handler:%s from environ', handler)
211
214
212 if not handler:
215 if not handler:
213 log.debug('request start: checking if request for `%s` is of VCS type in order: %s', path_url, backends)
216 log.debug('request start: checking if request for `%s:%s` is of VCS type in order: %s',
217 req_method, path_url, backends)
214 for vcs_type in backends:
218 for vcs_type in backends:
215 vcs_check, _handler = checks[vcs_type]
219 vcs_check, _handler = checks[vcs_type]
216 if vcs_check(environ):
220 if vcs_check(environ):
217 log.debug('vcs handler found %s', _handler)
221 log.debug('vcs handler found %s', _handler)
218 handler = _handler
222 handler = _handler
219 break
223 break
220
224
221 return handler
225 return handler
222
226
223
227
224 class VCSMiddleware(object):
228 class VCSMiddleware(object):
225
229
226 def __init__(self, app, registry, config, appenlight_client):
230 def __init__(self, app, registry, config, appenlight_client):
227 self.application = app
231 self.application = app
228 self.registry = registry
232 self.registry = registry
229 self.config = config
233 self.config = config
230 self.appenlight_client = appenlight_client
234 self.appenlight_client = appenlight_client
231 self.use_gzip = True
235 self.use_gzip = True
232 # order in which we check the middlewares, based on vcs.backends config
236 # order in which we check the middlewares, based on vcs.backends config
233 self.check_middlewares = config['vcs.backends']
237 self.check_middlewares = config['vcs.backends']
234
238
235 def vcs_config(self, repo_name=None):
239 def vcs_config(self, repo_name=None):
236 """
240 """
237 returns serialized VcsSettings
241 returns serialized VcsSettings
238 """
242 """
239 try:
243 try:
240 return VcsSettingsModel(
244 return VcsSettingsModel(
241 repo=repo_name).get_ui_settings_as_config_obj()
245 repo=repo_name).get_ui_settings_as_config_obj()
242 except Exception:
246 except Exception:
243 pass
247 pass
244
248
245 def wrap_in_gzip_if_enabled(self, app, config):
249 def wrap_in_gzip_if_enabled(self, app, config):
246 if self.use_gzip:
250 if self.use_gzip:
247 app = GunzipMiddleware(app)
251 app = GunzipMiddleware(app)
248 return app
252 return app
249
253
250 def _get_handler_app(self, environ):
254 def _get_handler_app(self, environ):
251 app = None
255 app = None
252 log.debug('VCSMiddleware: detecting vcs type.')
256 log.debug('VCSMiddleware: detecting vcs type.')
253 handler = detect_vcs_request(environ, self.check_middlewares)
257 handler = detect_vcs_request(environ, self.check_middlewares)
254 if handler:
258 if handler:
255 app = handler(self.config, self.registry)
259 app = handler(self.config, self.registry)
256
260
257 return app
261 return app
258
262
259 def __call__(self, environ, start_response):
263 def __call__(self, environ, start_response):
260 # check if we handle one of interesting protocols, optionally extract
264 # check if we handle one of interesting protocols, optionally extract
261 # specific vcsSettings and allow changes of how things are wrapped
265 # specific vcsSettings and allow changes of how things are wrapped
262 vcs_handler = self._get_handler_app(environ)
266 vcs_handler = self._get_handler_app(environ)
263 if vcs_handler:
267 if vcs_handler:
264 # translate the _REPO_ID into real repo NAME for usage
268 # translate the _REPO_ID into real repo NAME for usage
265 # in middleware
269 # in middleware
266
270
267 path_info = get_path_info(environ)
271 path_info = get_path_info(environ)
268 environ['PATH_INFO'] = vcs_handler._get_by_id(path_info)
272 environ['PATH_INFO'] = vcs_handler._get_by_id(path_info)
269
273
270 # Set acl, url and vcs repo names.
274 # Set acl, url and vcs repo names.
271 vcs_handler.set_repo_names(environ)
275 vcs_handler.set_repo_names(environ)
272
276
273 # register repo config back to the handler
277 # register repo config back to the handler
274 vcs_conf = self.vcs_config(vcs_handler.acl_repo_name)
278 vcs_conf = self.vcs_config(vcs_handler.acl_repo_name)
275 # maybe damaged/non existent settings. We still want to
279 # maybe damaged/non existent settings. We still want to
276 # pass that point to validate on is_valid_and_existing_repo
280 # pass that point to validate on is_valid_and_existing_repo
277 # and return proper HTTP Code back to client
281 # and return proper HTTP Code back to client
278 if vcs_conf:
282 if vcs_conf:
279 vcs_handler.repo_vcs_config = vcs_conf
283 vcs_handler.repo_vcs_config = vcs_conf
280
284
281 # check for type, presence in database and on filesystem
285 # check for type, presence in database and on filesystem
282 if not vcs_handler.is_valid_and_existing_repo(
286 if not vcs_handler.is_valid_and_existing_repo(
283 vcs_handler.acl_repo_name,
287 vcs_handler.acl_repo_name,
284 vcs_handler.base_path,
288 vcs_handler.base_path,
285 vcs_handler.SCM):
289 vcs_handler.SCM):
286 return HTTPNotFound()(environ, start_response)
290 return HTTPNotFound()(environ, start_response)
287
291
288 environ['REPO_NAME'] = vcs_handler.url_repo_name
292 environ['REPO_NAME'] = vcs_handler.url_repo_name
289
293
290 # Wrap handler in middlewares if they are enabled.
294 # Wrap handler in middlewares if they are enabled.
291 vcs_handler = self.wrap_in_gzip_if_enabled(
295 vcs_handler = self.wrap_in_gzip_if_enabled(
292 vcs_handler, self.config)
296 vcs_handler, self.config)
293 vcs_handler, _ = wrap_in_appenlight_if_enabled(
297 vcs_handler, _ = wrap_in_appenlight_if_enabled(
294 vcs_handler, self.config, self.appenlight_client)
298 vcs_handler, self.config, self.appenlight_client)
295
299
296 return vcs_handler(environ, start_response)
300 return vcs_handler(environ, start_response)
297
301
298 return self.application(environ, start_response)
302 return self.application(environ, start_response)
General Comments 0
You need to be logged in to leave comments. Login now