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