##// END OF EJS Templates
svn: fix usage of http exception from webob to pyramid one....
marcink -
r2790:ca04169f stable
parent child Browse files
Show More
@@ -1,204 +1,204 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2010-2018 RhodeCode GmbH
3 # Copyright (C) 2010-2018 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21 import base64
21 import base64
22 import logging
22 import logging
23 import urllib
23 import urllib
24 from urlparse import urljoin
24 from urlparse import urljoin
25
25
26 import requests
26 import requests
27 from webob.exc import HTTPNotAcceptable
27 from pyramid.httpexceptions import HTTPNotAcceptable
28
28
29 from rhodecode.lib import caches
29 from rhodecode.lib import caches
30 from rhodecode.lib.middleware import simplevcs
30 from rhodecode.lib.middleware import simplevcs
31 from rhodecode.lib.utils import is_valid_repo
31 from rhodecode.lib.utils import is_valid_repo
32 from rhodecode.lib.utils2 import str2bool, safe_int
32 from rhodecode.lib.utils2 import str2bool, safe_int
33 from rhodecode.lib.ext_json import json
33 from rhodecode.lib.ext_json import json
34 from rhodecode.lib.hooks_daemon import store_txn_id_data
34 from rhodecode.lib.hooks_daemon import store_txn_id_data
35
35
36
36
37 log = logging.getLogger(__name__)
37 log = logging.getLogger(__name__)
38
38
39
39
40 class SimpleSvnApp(object):
40 class SimpleSvnApp(object):
41 IGNORED_HEADERS = [
41 IGNORED_HEADERS = [
42 'connection', 'keep-alive', 'content-encoding',
42 'connection', 'keep-alive', 'content-encoding',
43 'transfer-encoding', 'content-length']
43 'transfer-encoding', 'content-length']
44 rc_extras = {}
44 rc_extras = {}
45
45
46 def __init__(self, config):
46 def __init__(self, config):
47 self.config = config
47 self.config = config
48
48
49 def __call__(self, environ, start_response):
49 def __call__(self, environ, start_response):
50 request_headers = self._get_request_headers(environ)
50 request_headers = self._get_request_headers(environ)
51
51
52 data = environ['wsgi.input']
52 data = environ['wsgi.input']
53 # johbo: Avoid that we end up with sending the request in chunked
53 # johbo: Avoid that we end up with sending the request in chunked
54 # transfer encoding (mainly on Gunicorn). If we know the content
54 # transfer encoding (mainly on Gunicorn). If we know the content
55 # length, then we should transfer the payload in one request.
55 # length, then we should transfer the payload in one request.
56 if environ['REQUEST_METHOD'] == 'MKCOL' or 'CONTENT_LENGTH' in environ:
56 if environ['REQUEST_METHOD'] == 'MKCOL' or 'CONTENT_LENGTH' in environ:
57 data = data.read()
57 data = data.read()
58 if data.startswith('(create-txn-with-props'):
58 if data.startswith('(create-txn-with-props'):
59 # store on-the-fly our rc_extra using svn revision properties
59 # store on-the-fly our rc_extra using svn revision properties
60 # those can be read later on in hooks executed so we have a way
60 # those can be read later on in hooks executed so we have a way
61 # to pass in the data into svn hooks
61 # to pass in the data into svn hooks
62 rc_data = base64.urlsafe_b64encode(json.dumps(self.rc_extras))
62 rc_data = base64.urlsafe_b64encode(json.dumps(self.rc_extras))
63 rc_data_len = len(rc_data)
63 rc_data_len = len(rc_data)
64 # header defines data lenght, and serialized data
64 # header defines data lenght, and serialized data
65 skel = ' rc-scm-extras {} {}'.format(rc_data_len, rc_data)
65 skel = ' rc-scm-extras {} {}'.format(rc_data_len, rc_data)
66 data = data[:-2] + skel + '))'
66 data = data[:-2] + skel + '))'
67
67
68 log.debug('Calling: %s method via `%s`', environ['REQUEST_METHOD'],
68 log.debug('Calling: %s method via `%s`', environ['REQUEST_METHOD'],
69 self._get_url(environ['PATH_INFO']))
69 self._get_url(environ['PATH_INFO']))
70
70
71 response = requests.request(
71 response = requests.request(
72 environ['REQUEST_METHOD'], self._get_url(environ['PATH_INFO']),
72 environ['REQUEST_METHOD'], self._get_url(environ['PATH_INFO']),
73 data=data, headers=request_headers)
73 data=data, headers=request_headers)
74
74
75 if response.status_code not in [200, 401]:
75 if response.status_code not in [200, 401]:
76 if response.status_code >= 500:
76 if response.status_code >= 500:
77 log.error('Got SVN response:%s with text:`%s`',
77 log.error('Got SVN response:%s with text:`%s`',
78 response, response.text)
78 response, response.text)
79 else:
79 else:
80 log.debug('Got SVN response:%s with text:`%s`',
80 log.debug('Got SVN response:%s with text:`%s`',
81 response, response.text)
81 response, response.text)
82 else:
82 else:
83 log.debug('got response code: %s', response.status_code)
83 log.debug('got response code: %s', response.status_code)
84
84
85 response_headers = self._get_response_headers(response.headers)
85 response_headers = self._get_response_headers(response.headers)
86
86
87 if response.headers.get('SVN-Txn-name'):
87 if response.headers.get('SVN-Txn-name'):
88 svn_tx_id = response.headers.get('SVN-Txn-name')
88 svn_tx_id = response.headers.get('SVN-Txn-name')
89 txn_id = caches.compute_key_from_params(
89 txn_id = caches.compute_key_from_params(
90 self.config['repository'], svn_tx_id)
90 self.config['repository'], svn_tx_id)
91 port = safe_int(self.rc_extras['hooks_uri'].split(':')[-1])
91 port = safe_int(self.rc_extras['hooks_uri'].split(':')[-1])
92 store_txn_id_data(txn_id, {'port': port})
92 store_txn_id_data(txn_id, {'port': port})
93
93
94 start_response(
94 start_response(
95 '{} {}'.format(response.status_code, response.reason),
95 '{} {}'.format(response.status_code, response.reason),
96 response_headers)
96 response_headers)
97 return response.iter_content(chunk_size=1024)
97 return response.iter_content(chunk_size=1024)
98
98
99 def _get_url(self, path):
99 def _get_url(self, path):
100 url_path = urljoin(
100 url_path = urljoin(
101 self.config.get('subversion_http_server_url', ''), path)
101 self.config.get('subversion_http_server_url', ''), path)
102 url_path = urllib.quote(url_path, safe="/:=~+!$,;'")
102 url_path = urllib.quote(url_path, safe="/:=~+!$,;'")
103 return url_path
103 return url_path
104
104
105 def _get_request_headers(self, environ):
105 def _get_request_headers(self, environ):
106 headers = {}
106 headers = {}
107
107
108 for key in environ:
108 for key in environ:
109 if not key.startswith('HTTP_'):
109 if not key.startswith('HTTP_'):
110 continue
110 continue
111 new_key = key.split('_')
111 new_key = key.split('_')
112 new_key = [k.capitalize() for k in new_key[1:]]
112 new_key = [k.capitalize() for k in new_key[1:]]
113 new_key = '-'.join(new_key)
113 new_key = '-'.join(new_key)
114 headers[new_key] = environ[key]
114 headers[new_key] = environ[key]
115
115
116 if 'CONTENT_TYPE' in environ:
116 if 'CONTENT_TYPE' in environ:
117 headers['Content-Type'] = environ['CONTENT_TYPE']
117 headers['Content-Type'] = environ['CONTENT_TYPE']
118
118
119 if 'CONTENT_LENGTH' in environ:
119 if 'CONTENT_LENGTH' in environ:
120 headers['Content-Length'] = environ['CONTENT_LENGTH']
120 headers['Content-Length'] = environ['CONTENT_LENGTH']
121
121
122 return headers
122 return headers
123
123
124 def _get_response_headers(self, headers):
124 def _get_response_headers(self, headers):
125 headers = [
125 headers = [
126 (h, headers[h])
126 (h, headers[h])
127 for h in headers
127 for h in headers
128 if h.lower() not in self.IGNORED_HEADERS
128 if h.lower() not in self.IGNORED_HEADERS
129 ]
129 ]
130
130
131 return headers
131 return headers
132
132
133
133
134 class DisabledSimpleSvnApp(object):
134 class DisabledSimpleSvnApp(object):
135 def __init__(self, config):
135 def __init__(self, config):
136 self.config = config
136 self.config = config
137
137
138 def __call__(self, environ, start_response):
138 def __call__(self, environ, start_response):
139 reason = 'Cannot handle SVN call because: SVN HTTP Proxy is not enabled'
139 reason = 'Cannot handle SVN call because: SVN HTTP Proxy is not enabled'
140 log.warning(reason)
140 log.warning(reason)
141 return HTTPNotAcceptable(reason)(environ, start_response)
141 return HTTPNotAcceptable(reason)(environ, start_response)
142
142
143
143
144 class SimpleSvn(simplevcs.SimpleVCS):
144 class SimpleSvn(simplevcs.SimpleVCS):
145
145
146 SCM = 'svn'
146 SCM = 'svn'
147 READ_ONLY_COMMANDS = ('OPTIONS', 'PROPFIND', 'GET', 'REPORT')
147 READ_ONLY_COMMANDS = ('OPTIONS', 'PROPFIND', 'GET', 'REPORT')
148 DEFAULT_HTTP_SERVER = 'http://localhost:8090'
148 DEFAULT_HTTP_SERVER = 'http://localhost:8090'
149
149
150 def _get_repository_name(self, environ):
150 def _get_repository_name(self, environ):
151 """
151 """
152 Gets repository name out of PATH_INFO header
152 Gets repository name out of PATH_INFO header
153
153
154 :param environ: environ where PATH_INFO is stored
154 :param environ: environ where PATH_INFO is stored
155 """
155 """
156 path = environ['PATH_INFO'].split('!')
156 path = environ['PATH_INFO'].split('!')
157 repo_name = path[0].strip('/')
157 repo_name = path[0].strip('/')
158
158
159 # SVN includes the whole path in it's requests, including
159 # SVN includes the whole path in it's requests, including
160 # subdirectories inside the repo. Therefore we have to search for
160 # subdirectories inside the repo. Therefore we have to search for
161 # the repo root directory.
161 # the repo root directory.
162 if not is_valid_repo(
162 if not is_valid_repo(
163 repo_name, self.base_path, explicit_scm=self.SCM):
163 repo_name, self.base_path, explicit_scm=self.SCM):
164 current_path = ''
164 current_path = ''
165 for component in repo_name.split('/'):
165 for component in repo_name.split('/'):
166 current_path += component
166 current_path += component
167 if is_valid_repo(
167 if is_valid_repo(
168 current_path, self.base_path, explicit_scm=self.SCM):
168 current_path, self.base_path, explicit_scm=self.SCM):
169 return current_path
169 return current_path
170 current_path += '/'
170 current_path += '/'
171
171
172 return repo_name
172 return repo_name
173
173
174 def _get_action(self, environ):
174 def _get_action(self, environ):
175 return (
175 return (
176 'pull'
176 'pull'
177 if environ['REQUEST_METHOD'] in self.READ_ONLY_COMMANDS
177 if environ['REQUEST_METHOD'] in self.READ_ONLY_COMMANDS
178 else 'push')
178 else 'push')
179
179
180 def _should_use_callback_daemon(self, extras, environ, action):
180 def _should_use_callback_daemon(self, extras, environ, action):
181 # only MERGE command triggers hooks, so we don't want to start
181 # only MERGE command triggers hooks, so we don't want to start
182 # hooks server too many times. POST however starts the svn transaction
182 # hooks server too many times. POST however starts the svn transaction
183 # so we also need to run the init of callback daemon of POST
183 # so we also need to run the init of callback daemon of POST
184 if environ['REQUEST_METHOD'] in ['MERGE', 'POST']:
184 if environ['REQUEST_METHOD'] in ['MERGE', 'POST']:
185 return True
185 return True
186 return False
186 return False
187
187
188 def _create_wsgi_app(self, repo_path, repo_name, config):
188 def _create_wsgi_app(self, repo_path, repo_name, config):
189 if self._is_svn_enabled():
189 if self._is_svn_enabled():
190 return SimpleSvnApp(config)
190 return SimpleSvnApp(config)
191 # we don't have http proxy enabled return dummy request handler
191 # we don't have http proxy enabled return dummy request handler
192 return DisabledSimpleSvnApp(config)
192 return DisabledSimpleSvnApp(config)
193
193
194 def _is_svn_enabled(self):
194 def _is_svn_enabled(self):
195 conf = self.repo_vcs_config
195 conf = self.repo_vcs_config
196 return str2bool(conf.get('vcs_svn_proxy', 'http_requests_enabled'))
196 return str2bool(conf.get('vcs_svn_proxy', 'http_requests_enabled'))
197
197
198 def _create_config(self, extras, repo_name):
198 def _create_config(self, extras, repo_name):
199 conf = self.repo_vcs_config
199 conf = self.repo_vcs_config
200 server_url = conf.get('vcs_svn_proxy', 'http_server_url')
200 server_url = conf.get('vcs_svn_proxy', 'http_server_url')
201 server_url = server_url or self.DEFAULT_HTTP_SERVER
201 server_url = server_url or self.DEFAULT_HTTP_SERVER
202
202
203 extras['subversion_http_server_url'] = server_url
203 extras['subversion_http_server_url'] = server_url
204 return extras
204 return extras
General Comments 0
You need to be logged in to leave comments. Login now