##// END OF EJS Templates
svn: escape special chars to allow interactions with non-standard svn paths....
marcink -
r1586:23640ce6 default
parent child Browse files
Show More
@@ -1,157 +1,169 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2010-2017 RhodeCode GmbH
3 # Copyright (C) 2010-2017 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 logging
21 import logging
22 import urllib
22 from urlparse import urljoin
23 from urlparse import urljoin
23
24
25
24 import requests
26 import requests
25 from webob.exc import HTTPNotAcceptable
27 from webob.exc import HTTPNotAcceptable
26
28
27 from rhodecode.lib.middleware import simplevcs
29 from rhodecode.lib.middleware import simplevcs
28 from rhodecode.lib.utils import is_valid_repo
30 from rhodecode.lib.utils import is_valid_repo
29 from rhodecode.lib.utils2 import str2bool
31 from rhodecode.lib.utils2 import str2bool
30
32
31 log = logging.getLogger(__name__)
33 log = logging.getLogger(__name__)
32
34
33
35
34 class SimpleSvnApp(object):
36 class SimpleSvnApp(object):
35 IGNORED_HEADERS = [
37 IGNORED_HEADERS = [
36 'connection', 'keep-alive', 'content-encoding',
38 'connection', 'keep-alive', 'content-encoding',
37 'transfer-encoding', 'content-length']
39 'transfer-encoding', 'content-length']
38
40
39 def __init__(self, config):
41 def __init__(self, config):
40 self.config = config
42 self.config = config
41
43
42 def __call__(self, environ, start_response):
44 def __call__(self, environ, start_response):
43 request_headers = self._get_request_headers(environ)
45 request_headers = self._get_request_headers(environ)
44
46
45 data = environ['wsgi.input']
47 data = environ['wsgi.input']
46 # johbo: Avoid that we end up with sending the request in chunked
48 # johbo: Avoid that we end up with sending the request in chunked
47 # transfer encoding (mainly on Gunicorn). If we know the content
49 # transfer encoding (mainly on Gunicorn). If we know the content
48 # length, then we should transfer the payload in one request.
50 # length, then we should transfer the payload in one request.
49 if environ['REQUEST_METHOD'] == 'MKCOL' or 'CONTENT_LENGTH' in environ:
51 if environ['REQUEST_METHOD'] == 'MKCOL' or 'CONTENT_LENGTH' in environ:
50 data = data.read()
52 data = data.read()
51
53
52 log.debug('Calling: %s method via `%s`', environ['REQUEST_METHOD'],
54 log.debug('Calling: %s method via `%s`', environ['REQUEST_METHOD'],
53 self._get_url(environ['PATH_INFO']))
55 self._get_url(environ['PATH_INFO']))
54 response = requests.request(
56 response = requests.request(
55 environ['REQUEST_METHOD'], self._get_url(environ['PATH_INFO']),
57 environ['REQUEST_METHOD'], self._get_url(environ['PATH_INFO']),
56 data=data, headers=request_headers)
58 data=data, headers=request_headers)
57
59
60 if response.status_code not in [200, 401]:
61 if response.status_code >= 500:
62 log.error('Got SVN response:%s with text:`%s`',
63 response, response.text)
64 else:
65 log.debug('Got SVN response:%s with text:`%s`',
66 response, response.text)
67
58 response_headers = self._get_response_headers(response.headers)
68 response_headers = self._get_response_headers(response.headers)
59 start_response(
69 start_response(
60 '{} {}'.format(response.status_code, response.reason),
70 '{} {}'.format(response.status_code, response.reason),
61 response_headers)
71 response_headers)
62 return response.iter_content(chunk_size=1024)
72 return response.iter_content(chunk_size=1024)
63
73
64 def _get_url(self, path):
74 def _get_url(self, path):
65 return urljoin(
75 url_path = urljoin(
66 self.config.get('subversion_http_server_url', ''), path)
76 self.config.get('subversion_http_server_url', ''), path)
77 url_path = urllib.quote(url_path, safe="/:=~+!$,;'")
78 return url_path
67
79
68 def _get_request_headers(self, environ):
80 def _get_request_headers(self, environ):
69 headers = {}
81 headers = {}
70
82
71 for key in environ:
83 for key in environ:
72 if not key.startswith('HTTP_'):
84 if not key.startswith('HTTP_'):
73 continue
85 continue
74 new_key = key.split('_')
86 new_key = key.split('_')
75 new_key = [k.capitalize() for k in new_key[1:]]
87 new_key = [k.capitalize() for k in new_key[1:]]
76 new_key = '-'.join(new_key)
88 new_key = '-'.join(new_key)
77 headers[new_key] = environ[key]
89 headers[new_key] = environ[key]
78
90
79 if 'CONTENT_TYPE' in environ:
91 if 'CONTENT_TYPE' in environ:
80 headers['Content-Type'] = environ['CONTENT_TYPE']
92 headers['Content-Type'] = environ['CONTENT_TYPE']
81
93
82 if 'CONTENT_LENGTH' in environ:
94 if 'CONTENT_LENGTH' in environ:
83 headers['Content-Length'] = environ['CONTENT_LENGTH']
95 headers['Content-Length'] = environ['CONTENT_LENGTH']
84
96
85 return headers
97 return headers
86
98
87 def _get_response_headers(self, headers):
99 def _get_response_headers(self, headers):
88 headers = [
100 headers = [
89 (h, headers[h])
101 (h, headers[h])
90 for h in headers
102 for h in headers
91 if h.lower() not in self.IGNORED_HEADERS
103 if h.lower() not in self.IGNORED_HEADERS
92 ]
104 ]
93
105
94 return headers
106 return headers
95
107
96
108
97 class DisabledSimpleSvnApp(object):
109 class DisabledSimpleSvnApp(object):
98 def __init__(self, config):
110 def __init__(self, config):
99 self.config = config
111 self.config = config
100
112
101 def __call__(self, environ, start_response):
113 def __call__(self, environ, start_response):
102 reason = 'Cannot handle SVN call because: SVN HTTP Proxy is not enabled'
114 reason = 'Cannot handle SVN call because: SVN HTTP Proxy is not enabled'
103 log.warning(reason)
115 log.warning(reason)
104 return HTTPNotAcceptable(reason)(environ, start_response)
116 return HTTPNotAcceptable(reason)(environ, start_response)
105
117
106
118
107 class SimpleSvn(simplevcs.SimpleVCS):
119 class SimpleSvn(simplevcs.SimpleVCS):
108
120
109 SCM = 'svn'
121 SCM = 'svn'
110 READ_ONLY_COMMANDS = ('OPTIONS', 'PROPFIND', 'GET', 'REPORT')
122 READ_ONLY_COMMANDS = ('OPTIONS', 'PROPFIND', 'GET', 'REPORT')
111 DEFAULT_HTTP_SERVER = 'http://localhost:8090'
123 DEFAULT_HTTP_SERVER = 'http://localhost:8090'
112
124
113 def _get_repository_name(self, environ):
125 def _get_repository_name(self, environ):
114 """
126 """
115 Gets repository name out of PATH_INFO header
127 Gets repository name out of PATH_INFO header
116
128
117 :param environ: environ where PATH_INFO is stored
129 :param environ: environ where PATH_INFO is stored
118 """
130 """
119 path = environ['PATH_INFO'].split('!')
131 path = environ['PATH_INFO'].split('!')
120 repo_name = path[0].strip('/')
132 repo_name = path[0].strip('/')
121
133
122 # SVN includes the whole path in it's requests, including
134 # SVN includes the whole path in it's requests, including
123 # subdirectories inside the repo. Therefore we have to search for
135 # subdirectories inside the repo. Therefore we have to search for
124 # the repo root directory.
136 # the repo root directory.
125 if not is_valid_repo(repo_name, self.basepath, self.SCM):
137 if not is_valid_repo(repo_name, self.basepath, self.SCM):
126 current_path = ''
138 current_path = ''
127 for component in repo_name.split('/'):
139 for component in repo_name.split('/'):
128 current_path += component
140 current_path += component
129 if is_valid_repo(current_path, self.basepath, self.SCM):
141 if is_valid_repo(current_path, self.basepath, self.SCM):
130 return current_path
142 return current_path
131 current_path += '/'
143 current_path += '/'
132
144
133 return repo_name
145 return repo_name
134
146
135 def _get_action(self, environ):
147 def _get_action(self, environ):
136 return (
148 return (
137 'pull'
149 'pull'
138 if environ['REQUEST_METHOD'] in self.READ_ONLY_COMMANDS
150 if environ['REQUEST_METHOD'] in self.READ_ONLY_COMMANDS
139 else 'push')
151 else 'push')
140
152
141 def _create_wsgi_app(self, repo_path, repo_name, config):
153 def _create_wsgi_app(self, repo_path, repo_name, config):
142 if self._is_svn_enabled():
154 if self._is_svn_enabled():
143 return SimpleSvnApp(config)
155 return SimpleSvnApp(config)
144 # we don't have http proxy enabled return dummy request handler
156 # we don't have http proxy enabled return dummy request handler
145 return DisabledSimpleSvnApp(config)
157 return DisabledSimpleSvnApp(config)
146
158
147 def _is_svn_enabled(self):
159 def _is_svn_enabled(self):
148 conf = self.repo_vcs_config
160 conf = self.repo_vcs_config
149 return str2bool(conf.get('vcs_svn_proxy', 'http_requests_enabled'))
161 return str2bool(conf.get('vcs_svn_proxy', 'http_requests_enabled'))
150
162
151 def _create_config(self, extras, repo_name):
163 def _create_config(self, extras, repo_name):
152 conf = self.repo_vcs_config
164 conf = self.repo_vcs_config
153 server_url = conf.get('vcs_svn_proxy', 'http_server_url')
165 server_url = conf.get('vcs_svn_proxy', 'http_server_url')
154 server_url = server_url or self.DEFAULT_HTTP_SERVER
166 server_url = server_url or self.DEFAULT_HTTP_SERVER
155
167
156 extras['subversion_http_server_url'] = server_url
168 extras['subversion_http_server_url'] = server_url
157 return extras
169 return extras
General Comments 0
You need to be logged in to leave comments. Login now