##// END OF EJS Templates
svn: handle non-ascii message editing.
marcink -
r3922:4891a760 default
parent child Browse files
Show More
@@ -1,228 +1,229 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2010-2019 RhodeCode GmbH
3 # Copyright (C) 2010-2019 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 import urlparse
24 import urlparse
25
25
26 import requests
26 import requests
27 from pyramid.httpexceptions import HTTPNotAcceptable
27 from pyramid.httpexceptions import HTTPNotAcceptable
28
28
29 from rhodecode.lib import rc_cache
29 from rhodecode.lib import rc_cache
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, safe_str
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 data = environ['wsgi.input']
51 data = environ['wsgi.input']
52 req_method = environ['REQUEST_METHOD']
52 req_method = environ['REQUEST_METHOD']
53 has_content_length = 'CONTENT_LENGTH' in environ
53 has_content_length = 'CONTENT_LENGTH' in environ
54 path_info = self._get_url(
54 path_info = self._get_url(
55 self.config.get('subversion_http_server_url', ''), environ['PATH_INFO'])
55 self.config.get('subversion_http_server_url', ''), environ['PATH_INFO'])
56 transfer_encoding = environ.get('HTTP_TRANSFER_ENCODING', '')
56 transfer_encoding = environ.get('HTTP_TRANSFER_ENCODING', '')
57 log.debug('Handling: %s method via `%s`', req_method, path_info)
57 log.debug('Handling: %s method via `%s`', req_method, path_info)
58
58
59 # stream control flag, based on request and content type...
59 # stream control flag, based on request and content type...
60 stream = False
60 stream = False
61
61
62 if req_method in ['MKCOL'] or has_content_length:
62 if req_method in ['MKCOL'] or has_content_length:
63 data_processed = False
63 data_processed = False
64 # read chunk to check if we have txn-with-props
64 # read chunk to check if we have txn-with-props
65 initial_data = data.read(1024)
65 initial_data = data.read(1024)
66 if initial_data.startswith('(create-txn-with-props'):
66 if initial_data.startswith('(create-txn-with-props'):
67 data = initial_data + data.read()
67 data = initial_data + data.read()
68 # store on-the-fly our rc_extra using svn revision properties
68 # store on-the-fly our rc_extra using svn revision properties
69 # those can be read later on in hooks executed so we have a way
69 # those can be read later on in hooks executed so we have a way
70 # to pass in the data into svn hooks
70 # to pass in the data into svn hooks
71 rc_data = base64.urlsafe_b64encode(json.dumps(self.rc_extras))
71 rc_data = base64.urlsafe_b64encode(json.dumps(self.rc_extras))
72 rc_data_len = len(rc_data)
72 rc_data_len = len(rc_data)
73 # header defines data length, and serialized data
73 # header defines data length, and serialized data
74 skel = ' rc-scm-extras {} {}'.format(rc_data_len, rc_data)
74 skel = ' rc-scm-extras {} {}'.format(rc_data_len, rc_data)
75 data = data[:-2] + skel + '))'
75 data = data[:-2] + skel + '))'
76 data_processed = True
76 data_processed = True
77
77
78 if not data_processed:
78 if not data_processed:
79 # NOTE(johbo): Avoid that we end up with sending the request in chunked
79 # NOTE(johbo): Avoid that we end up with sending the request in chunked
80 # transfer encoding (mainly on Gunicorn). If we know the content
80 # transfer encoding (mainly on Gunicorn). If we know the content
81 # length, then we should transfer the payload in one request.
81 # length, then we should transfer the payload in one request.
82 data = initial_data + data.read()
82 data = initial_data + data.read()
83
83
84 if req_method in ['GET', 'PUT'] or transfer_encoding == 'chunked':
84 if req_method in ['GET', 'PUT'] or transfer_encoding == 'chunked':
85 # NOTE(marcink): when getting/uploading files we want to STREAM content
85 # NOTE(marcink): when getting/uploading files we want to STREAM content
86 # back to the client/proxy instead of buffering it here...
86 # back to the client/proxy instead of buffering it here...
87 stream = True
87 stream = True
88
88
89 stream = stream
89 stream = stream
90 log.debug('Calling SVN PROXY at `%s`, using method:%s. Stream: %s',
90 log.debug('Calling SVN PROXY at `%s`, using method:%s. Stream: %s',
91 path_info, req_method, stream)
91 path_info, req_method, stream)
92 try:
92 try:
93 response = requests.request(
93 response = requests.request(
94 req_method, path_info,
94 req_method, path_info,
95 data=data, headers=request_headers, stream=stream)
95 data=data, headers=request_headers, stream=stream)
96 except requests.ConnectionError:
96 except requests.ConnectionError:
97 log.exception('ConnectionError occurred for endpoint %s', path_info)
97 log.exception('ConnectionError occurred for endpoint %s', path_info)
98 raise
98 raise
99
99
100 if response.status_code not in [200, 401]:
100 if response.status_code not in [200, 401]:
101 text = '\n{}'.format(response.text) if response.text else ''
101 from rhodecode.lib.utils2 import safe_str
102 text = '\n{}'.format(safe_str(response.text)) if response.text else ''
102 if response.status_code >= 500:
103 if response.status_code >= 500:
103 log.error('Got SVN response:%s with text:`%s`', response, text)
104 log.error('Got SVN response:%s with text:`%s`', response, text)
104 else:
105 else:
105 log.debug('Got SVN response:%s with text:`%s`', response, text)
106 log.debug('Got SVN response:%s with text:`%s`', response, text)
106 else:
107 else:
107 log.debug('got response code: %s', response.status_code)
108 log.debug('got response code: %s', response.status_code)
108
109
109 response_headers = self._get_response_headers(response.headers)
110 response_headers = self._get_response_headers(response.headers)
110
111
111 if response.headers.get('SVN-Txn-name'):
112 if response.headers.get('SVN-Txn-name'):
112 svn_tx_id = response.headers.get('SVN-Txn-name')
113 svn_tx_id = response.headers.get('SVN-Txn-name')
113 txn_id = rc_cache.utils.compute_key_from_params(
114 txn_id = rc_cache.utils.compute_key_from_params(
114 self.config['repository'], svn_tx_id)
115 self.config['repository'], svn_tx_id)
115 port = safe_int(self.rc_extras['hooks_uri'].split(':')[-1])
116 port = safe_int(self.rc_extras['hooks_uri'].split(':')[-1])
116 store_txn_id_data(txn_id, {'port': port})
117 store_txn_id_data(txn_id, {'port': port})
117
118
118 start_response(
119 start_response(
119 '{} {}'.format(response.status_code, response.reason),
120 '{} {}'.format(response.status_code, response.reason),
120 response_headers)
121 response_headers)
121 return response.iter_content(chunk_size=1024)
122 return response.iter_content(chunk_size=1024)
122
123
123 def _get_url(self, svn_http_server, path):
124 def _get_url(self, svn_http_server, path):
124 svn_http_server_url = (svn_http_server or '').rstrip('/')
125 svn_http_server_url = (svn_http_server or '').rstrip('/')
125 url_path = urlparse.urljoin(svn_http_server_url + '/', (path or '').lstrip('/'))
126 url_path = urlparse.urljoin(svn_http_server_url + '/', (path or '').lstrip('/'))
126 url_path = urllib.quote(url_path, safe="/:=~+!$,;'")
127 url_path = urllib.quote(url_path, safe="/:=~+!$,;'")
127 return url_path
128 return url_path
128
129
129 def _get_request_headers(self, environ):
130 def _get_request_headers(self, environ):
130 headers = {}
131 headers = {}
131
132
132 for key in environ:
133 for key in environ:
133 if not key.startswith('HTTP_'):
134 if not key.startswith('HTTP_'):
134 continue
135 continue
135 new_key = key.split('_')
136 new_key = key.split('_')
136 new_key = [k.capitalize() for k in new_key[1:]]
137 new_key = [k.capitalize() for k in new_key[1:]]
137 new_key = '-'.join(new_key)
138 new_key = '-'.join(new_key)
138 headers[new_key] = environ[key]
139 headers[new_key] = environ[key]
139
140
140 if 'CONTENT_TYPE' in environ:
141 if 'CONTENT_TYPE' in environ:
141 headers['Content-Type'] = environ['CONTENT_TYPE']
142 headers['Content-Type'] = environ['CONTENT_TYPE']
142
143
143 if 'CONTENT_LENGTH' in environ:
144 if 'CONTENT_LENGTH' in environ:
144 headers['Content-Length'] = environ['CONTENT_LENGTH']
145 headers['Content-Length'] = environ['CONTENT_LENGTH']
145
146
146 return headers
147 return headers
147
148
148 def _get_response_headers(self, headers):
149 def _get_response_headers(self, headers):
149 headers = [
150 headers = [
150 (h, headers[h])
151 (h, headers[h])
151 for h in headers
152 for h in headers
152 if h.lower() not in self.IGNORED_HEADERS
153 if h.lower() not in self.IGNORED_HEADERS
153 ]
154 ]
154
155
155 return headers
156 return headers
156
157
157
158
158 class DisabledSimpleSvnApp(object):
159 class DisabledSimpleSvnApp(object):
159 def __init__(self, config):
160 def __init__(self, config):
160 self.config = config
161 self.config = config
161
162
162 def __call__(self, environ, start_response):
163 def __call__(self, environ, start_response):
163 reason = 'Cannot handle SVN call because: SVN HTTP Proxy is not enabled'
164 reason = 'Cannot handle SVN call because: SVN HTTP Proxy is not enabled'
164 log.warning(reason)
165 log.warning(reason)
165 return HTTPNotAcceptable(reason)(environ, start_response)
166 return HTTPNotAcceptable(reason)(environ, start_response)
166
167
167
168
168 class SimpleSvn(simplevcs.SimpleVCS):
169 class SimpleSvn(simplevcs.SimpleVCS):
169
170
170 SCM = 'svn'
171 SCM = 'svn'
171 READ_ONLY_COMMANDS = ('OPTIONS', 'PROPFIND', 'GET', 'REPORT')
172 READ_ONLY_COMMANDS = ('OPTIONS', 'PROPFIND', 'GET', 'REPORT')
172 DEFAULT_HTTP_SERVER = 'http://localhost:8090'
173 DEFAULT_HTTP_SERVER = 'http://localhost:8090'
173
174
174 def _get_repository_name(self, environ):
175 def _get_repository_name(self, environ):
175 """
176 """
176 Gets repository name out of PATH_INFO header
177 Gets repository name out of PATH_INFO header
177
178
178 :param environ: environ where PATH_INFO is stored
179 :param environ: environ where PATH_INFO is stored
179 """
180 """
180 path = environ['PATH_INFO'].split('!')
181 path = environ['PATH_INFO'].split('!')
181 repo_name = path[0].strip('/')
182 repo_name = path[0].strip('/')
182
183
183 # SVN includes the whole path in it's requests, including
184 # SVN includes the whole path in it's requests, including
184 # subdirectories inside the repo. Therefore we have to search for
185 # subdirectories inside the repo. Therefore we have to search for
185 # the repo root directory.
186 # the repo root directory.
186 if not is_valid_repo(
187 if not is_valid_repo(
187 repo_name, self.base_path, explicit_scm=self.SCM):
188 repo_name, self.base_path, explicit_scm=self.SCM):
188 current_path = ''
189 current_path = ''
189 for component in repo_name.split('/'):
190 for component in repo_name.split('/'):
190 current_path += component
191 current_path += component
191 if is_valid_repo(
192 if is_valid_repo(
192 current_path, self.base_path, explicit_scm=self.SCM):
193 current_path, self.base_path, explicit_scm=self.SCM):
193 return current_path
194 return current_path
194 current_path += '/'
195 current_path += '/'
195
196
196 return repo_name
197 return repo_name
197
198
198 def _get_action(self, environ):
199 def _get_action(self, environ):
199 return (
200 return (
200 'pull'
201 'pull'
201 if environ['REQUEST_METHOD'] in self.READ_ONLY_COMMANDS
202 if environ['REQUEST_METHOD'] in self.READ_ONLY_COMMANDS
202 else 'push')
203 else 'push')
203
204
204 def _should_use_callback_daemon(self, extras, environ, action):
205 def _should_use_callback_daemon(self, extras, environ, action):
205 # only MERGE command triggers hooks, so we don't want to start
206 # only MERGE command triggers hooks, so we don't want to start
206 # hooks server too many times. POST however starts the svn transaction
207 # hooks server too many times. POST however starts the svn transaction
207 # so we also need to run the init of callback daemon of POST
208 # so we also need to run the init of callback daemon of POST
208 if environ['REQUEST_METHOD'] in ['MERGE', 'POST']:
209 if environ['REQUEST_METHOD'] in ['MERGE', 'POST']:
209 return True
210 return True
210 return False
211 return False
211
212
212 def _create_wsgi_app(self, repo_path, repo_name, config):
213 def _create_wsgi_app(self, repo_path, repo_name, config):
213 if self._is_svn_enabled():
214 if self._is_svn_enabled():
214 return SimpleSvnApp(config)
215 return SimpleSvnApp(config)
215 # we don't have http proxy enabled return dummy request handler
216 # we don't have http proxy enabled return dummy request handler
216 return DisabledSimpleSvnApp(config)
217 return DisabledSimpleSvnApp(config)
217
218
218 def _is_svn_enabled(self):
219 def _is_svn_enabled(self):
219 conf = self.repo_vcs_config
220 conf = self.repo_vcs_config
220 return str2bool(conf.get('vcs_svn_proxy', 'http_requests_enabled'))
221 return str2bool(conf.get('vcs_svn_proxy', 'http_requests_enabled'))
221
222
222 def _create_config(self, extras, repo_name, scheme='http'):
223 def _create_config(self, extras, repo_name, scheme='http'):
223 conf = self.repo_vcs_config
224 conf = self.repo_vcs_config
224 server_url = conf.get('vcs_svn_proxy', 'http_server_url')
225 server_url = conf.get('vcs_svn_proxy', 'http_server_url')
225 server_url = server_url or self.DEFAULT_HTTP_SERVER
226 server_url = server_url or self.DEFAULT_HTTP_SERVER
226
227
227 extras['subversion_http_server_url'] = server_url
228 extras['subversion_http_server_url'] = server_url
228 return extras
229 return extras
General Comments 0
You need to be logged in to leave comments. Login now