##// END OF EJS Templates
vcs: Remove custom response header which contains backend key....
Martin Bornhold -
r950:21c331ba default
parent child Browse files
Show More
@@ -1,159 +1,155 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2010-2016 RhodeCode GmbH
3 # Copyright (C) 2010-2016 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 from urlparse import urljoin
22 from urlparse import urljoin
23
23
24 import requests
24 import requests
25 from webob.exc import HTTPNotAcceptable
25 from webob.exc import HTTPNotAcceptable
26
26
27 from rhodecode.lib.middleware import simplevcs
27 from rhodecode.lib.middleware import simplevcs
28 from rhodecode.lib.utils import is_valid_repo
28 from rhodecode.lib.utils import is_valid_repo
29 from rhodecode.lib.utils2 import str2bool
29 from rhodecode.lib.utils2 import str2bool
30
30
31 log = logging.getLogger(__name__)
31 log = logging.getLogger(__name__)
32
32
33
33
34 class SimpleSvnApp(object):
34 class SimpleSvnApp(object):
35 IGNORED_HEADERS = [
35 IGNORED_HEADERS = [
36 'connection', 'keep-alive', 'content-encoding',
36 'connection', 'keep-alive', 'content-encoding',
37 'transfer-encoding', 'content-length']
37 'transfer-encoding', 'content-length']
38
38
39 def __init__(self, config):
39 def __init__(self, config):
40 self.config = config
40 self.config = config
41
41
42 def __call__(self, environ, start_response):
42 def __call__(self, environ, start_response):
43 request_headers = self._get_request_headers(environ)
43 request_headers = self._get_request_headers(environ)
44
44
45 data = environ['wsgi.input']
45 data = environ['wsgi.input']
46 # johbo: Avoid that we end up with sending the request in chunked
46 # johbo: Avoid that we end up with sending the request in chunked
47 # transfer encoding (mainly on Gunicorn). If we know the content
47 # transfer encoding (mainly on Gunicorn). If we know the content
48 # length, then we should transfer the payload in one request.
48 # length, then we should transfer the payload in one request.
49 if environ['REQUEST_METHOD'] == 'MKCOL' or 'CONTENT_LENGTH' in environ:
49 if environ['REQUEST_METHOD'] == 'MKCOL' or 'CONTENT_LENGTH' in environ:
50 data = data.read()
50 data = data.read()
51
51
52 response = requests.request(
52 response = requests.request(
53 environ['REQUEST_METHOD'], self._get_url(environ['PATH_INFO']),
53 environ['REQUEST_METHOD'], self._get_url(environ['PATH_INFO']),
54 data=data, headers=request_headers)
54 data=data, headers=request_headers)
55
55
56 response_headers = self._get_response_headers(response.headers)
56 response_headers = self._get_response_headers(response.headers)
57 start_response(
57 start_response(
58 '{} {}'.format(response.status_code, response.reason),
58 '{} {}'.format(response.status_code, response.reason),
59 response_headers)
59 response_headers)
60 return response.iter_content(chunk_size=1024)
60 return response.iter_content(chunk_size=1024)
61
61
62 def _get_url(self, path):
62 def _get_url(self, path):
63 return urljoin(
63 return urljoin(
64 self.config.get('subversion_http_server_url', ''), path)
64 self.config.get('subversion_http_server_url', ''), path)
65
65
66 def _get_request_headers(self, environ):
66 def _get_request_headers(self, environ):
67 headers = {}
67 headers = {}
68
68
69 for key in environ:
69 for key in environ:
70 if not key.startswith('HTTP_'):
70 if not key.startswith('HTTP_'):
71 continue
71 continue
72 new_key = key.split('_')
72 new_key = key.split('_')
73 new_key = [k.capitalize() for k in new_key[1:]]
73 new_key = [k.capitalize() for k in new_key[1:]]
74 new_key = '-'.join(new_key)
74 new_key = '-'.join(new_key)
75 headers[new_key] = environ[key]
75 headers[new_key] = environ[key]
76
76
77 if 'CONTENT_TYPE' in environ:
77 if 'CONTENT_TYPE' in environ:
78 headers['Content-Type'] = environ['CONTENT_TYPE']
78 headers['Content-Type'] = environ['CONTENT_TYPE']
79
79
80 if 'CONTENT_LENGTH' in environ:
80 if 'CONTENT_LENGTH' in environ:
81 headers['Content-Length'] = environ['CONTENT_LENGTH']
81 headers['Content-Length'] = environ['CONTENT_LENGTH']
82
82
83 return headers
83 return headers
84
84
85 def _get_response_headers(self, headers):
85 def _get_response_headers(self, headers):
86 headers = [
86 headers = [
87 (h, headers[h])
87 (h, headers[h])
88 for h in headers
88 for h in headers
89 if h.lower() not in self.IGNORED_HEADERS
89 if h.lower() not in self.IGNORED_HEADERS
90 ]
90 ]
91
91
92 # Add custom response header to indicate that this is a VCS response
93 # and which backend is used.
94 headers.append(('X-RhodeCode-Backend', 'svn'))
95
96 return headers
92 return headers
97
93
98
94
99 class DisabledSimpleSvnApp(object):
95 class DisabledSimpleSvnApp(object):
100 def __init__(self, config):
96 def __init__(self, config):
101 self.config = config
97 self.config = config
102
98
103 def __call__(self, environ, start_response):
99 def __call__(self, environ, start_response):
104 reason = 'Cannot handle SVN call because: SVN HTTP Proxy is not enabled'
100 reason = 'Cannot handle SVN call because: SVN HTTP Proxy is not enabled'
105 log.warning(reason)
101 log.warning(reason)
106 return HTTPNotAcceptable(reason)(environ, start_response)
102 return HTTPNotAcceptable(reason)(environ, start_response)
107
103
108
104
109 class SimpleSvn(simplevcs.SimpleVCS):
105 class SimpleSvn(simplevcs.SimpleVCS):
110
106
111 SCM = 'svn'
107 SCM = 'svn'
112 READ_ONLY_COMMANDS = ('OPTIONS', 'PROPFIND', 'GET', 'REPORT')
108 READ_ONLY_COMMANDS = ('OPTIONS', 'PROPFIND', 'GET', 'REPORT')
113 DEFAULT_HTTP_SERVER = 'http://localhost:8090'
109 DEFAULT_HTTP_SERVER = 'http://localhost:8090'
114
110
115 def _get_repository_name(self, environ):
111 def _get_repository_name(self, environ):
116 """
112 """
117 Gets repository name out of PATH_INFO header
113 Gets repository name out of PATH_INFO header
118
114
119 :param environ: environ where PATH_INFO is stored
115 :param environ: environ where PATH_INFO is stored
120 """
116 """
121 path = environ['PATH_INFO'].split('!')
117 path = environ['PATH_INFO'].split('!')
122 repo_name = path[0].strip('/')
118 repo_name = path[0].strip('/')
123
119
124 # SVN includes the whole path in it's requests, including
120 # SVN includes the whole path in it's requests, including
125 # subdirectories inside the repo. Therefore we have to search for
121 # subdirectories inside the repo. Therefore we have to search for
126 # the repo root directory.
122 # the repo root directory.
127 if not is_valid_repo(repo_name, self.basepath, self.SCM):
123 if not is_valid_repo(repo_name, self.basepath, self.SCM):
128 current_path = ''
124 current_path = ''
129 for component in repo_name.split('/'):
125 for component in repo_name.split('/'):
130 current_path += component
126 current_path += component
131 if is_valid_repo(current_path, self.basepath, self.SCM):
127 if is_valid_repo(current_path, self.basepath, self.SCM):
132 return current_path
128 return current_path
133 current_path += '/'
129 current_path += '/'
134
130
135 return repo_name
131 return repo_name
136
132
137 def _get_action(self, environ):
133 def _get_action(self, environ):
138 return (
134 return (
139 'pull'
135 'pull'
140 if environ['REQUEST_METHOD'] in self.READ_ONLY_COMMANDS
136 if environ['REQUEST_METHOD'] in self.READ_ONLY_COMMANDS
141 else 'push')
137 else 'push')
142
138
143 def _create_wsgi_app(self, repo_path, repo_name, config):
139 def _create_wsgi_app(self, repo_path, repo_name, config):
144 if self._is_svn_enabled():
140 if self._is_svn_enabled():
145 return SimpleSvnApp(config)
141 return SimpleSvnApp(config)
146 # we don't have http proxy enabled return dummy request handler
142 # we don't have http proxy enabled return dummy request handler
147 return DisabledSimpleSvnApp(config)
143 return DisabledSimpleSvnApp(config)
148
144
149 def _is_svn_enabled(self):
145 def _is_svn_enabled(self):
150 conf = self.repo_vcs_config
146 conf = self.repo_vcs_config
151 return str2bool(conf.get('vcs_svn_proxy', 'http_requests_enabled'))
147 return str2bool(conf.get('vcs_svn_proxy', 'http_requests_enabled'))
152
148
153 def _create_config(self, extras, repo_name):
149 def _create_config(self, extras, repo_name):
154 conf = self.repo_vcs_config
150 conf = self.repo_vcs_config
155 server_url = conf.get('vcs_svn_proxy', 'http_server_url')
151 server_url = conf.get('vcs_svn_proxy', 'http_server_url')
156 server_url = server_url or self.DEFAULT_HTTP_SERVER
152 server_url = server_url or self.DEFAULT_HTTP_SERVER
157
153
158 extras['subversion_http_server_url'] = server_url
154 extras['subversion_http_server_url'] = server_url
159 return extras
155 return extras
@@ -1,140 +1,136 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2014-2016 RhodeCode GmbH
3 # Copyright (C) 2014-2016 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 Implementation of the scm_app interface using raw HTTP communication.
22 Implementation of the scm_app interface using raw HTTP communication.
23 """
23 """
24
24
25 import base64
25 import base64
26 import logging
26 import logging
27 import urlparse
27 import urlparse
28 import wsgiref.util
28 import wsgiref.util
29
29
30 import msgpack
30 import msgpack
31 import requests
31 import requests
32 import webob.request
32 import webob.request
33
33
34 import rhodecode
34 import rhodecode
35
35
36
36
37 log = logging.getLogger(__name__)
37 log = logging.getLogger(__name__)
38
38
39
39
40 def create_git_wsgi_app(repo_path, repo_name, config, backend):
40 def create_git_wsgi_app(repo_path, repo_name, config, backend):
41 url = _vcs_streaming_url() + 'git/'
41 url = _vcs_streaming_url() + 'git/'
42 return VcsHttpProxy(url, repo_path, repo_name, config, backend)
42 return VcsHttpProxy(url, repo_path, repo_name, config, backend)
43
43
44
44
45 def create_hg_wsgi_app(repo_path, repo_name, config, backend):
45 def create_hg_wsgi_app(repo_path, repo_name, config, backend):
46 url = _vcs_streaming_url() + 'hg/'
46 url = _vcs_streaming_url() + 'hg/'
47 return VcsHttpProxy(url, repo_path, repo_name, config, backend)
47 return VcsHttpProxy(url, repo_path, repo_name, config, backend)
48
48
49
49
50 def _vcs_streaming_url():
50 def _vcs_streaming_url():
51 template = 'http://{}/stream/'
51 template = 'http://{}/stream/'
52 return template.format(rhodecode.CONFIG['vcs.server'])
52 return template.format(rhodecode.CONFIG['vcs.server'])
53
53
54
54
55 # TODO: johbo: Avoid the global.
55 # TODO: johbo: Avoid the global.
56 session = requests.Session()
56 session = requests.Session()
57 # Requests speedup, avoid reading .netrc and similar
57 # Requests speedup, avoid reading .netrc and similar
58 session.trust_env = False
58 session.trust_env = False
59
59
60
60
61 class VcsHttpProxy(object):
61 class VcsHttpProxy(object):
62 """
62 """
63 A WSGI application which proxies vcs requests.
63 A WSGI application which proxies vcs requests.
64
64
65 The goal is to shuffle the data around without touching it. The only
65 The goal is to shuffle the data around without touching it. The only
66 exception is the extra data from the config object which we send to the
66 exception is the extra data from the config object which we send to the
67 server as well.
67 server as well.
68 """
68 """
69
69
70 def __init__(self, url, repo_path, repo_name, config, backend):
70 def __init__(self, url, repo_path, repo_name, config, backend):
71 """
71 """
72 :param str url: The URL of the VCSServer to call.
72 :param str url: The URL of the VCSServer to call.
73 """
73 """
74 self._url = url
74 self._url = url
75 self._repo_name = repo_name
75 self._repo_name = repo_name
76 self._repo_path = repo_path
76 self._repo_path = repo_path
77 self._config = config
77 self._config = config
78 self._backend = backend
78 self._backend = backend
79 log.debug(
79 log.debug(
80 "Creating VcsHttpProxy for repo %s, url %s",
80 "Creating VcsHttpProxy for repo %s, url %s",
81 repo_name, url)
81 repo_name, url)
82
82
83 def __call__(self, environ, start_response):
83 def __call__(self, environ, start_response):
84 status = '200 OK'
84 status = '200 OK'
85
85
86 config = msgpack.packb(self._config)
86 config = msgpack.packb(self._config)
87 request = webob.request.Request(environ)
87 request = webob.request.Request(environ)
88 request_headers = request.headers
88 request_headers = request.headers
89 request_headers.update({
89 request_headers.update({
90 # TODO: johbo: Remove this, rely on URL path only
90 # TODO: johbo: Remove this, rely on URL path only
91 'X-RC-Repo-Name': self._repo_name,
91 'X-RC-Repo-Name': self._repo_name,
92 'X-RC-Repo-Path': self._repo_path,
92 'X-RC-Repo-Path': self._repo_path,
93 'X-RC-Path-Info': environ['PATH_INFO'],
93 'X-RC-Path-Info': environ['PATH_INFO'],
94 # TODO: johbo: Avoid encoding and put this into payload?
94 # TODO: johbo: Avoid encoding and put this into payload?
95 'X-RC-Repo-Config': base64.b64encode(config),
95 'X-RC-Repo-Config': base64.b64encode(config),
96 })
96 })
97
97
98 data = environ['wsgi.input'].read()
98 data = environ['wsgi.input'].read()
99 method = environ['REQUEST_METHOD']
99 method = environ['REQUEST_METHOD']
100
100
101 # Preserve the query string
101 # Preserve the query string
102 url = self._url
102 url = self._url
103 url = urlparse.urljoin(url, self._repo_name)
103 url = urlparse.urljoin(url, self._repo_name)
104 if environ.get('QUERY_STRING'):
104 if environ.get('QUERY_STRING'):
105 url += '?' + environ['QUERY_STRING']
105 url += '?' + environ['QUERY_STRING']
106
106
107 response = session.request(
107 response = session.request(
108 method, url,
108 method, url,
109 data=data,
109 data=data,
110 headers=request_headers,
110 headers=request_headers,
111 stream=True)
111 stream=True)
112
112
113 # Preserve the headers of the response, except hop_by_hop ones
113 # Preserve the headers of the response, except hop_by_hop ones
114 response_headers = [
114 response_headers = [
115 (h, v) for h, v in response.headers.items()
115 (h, v) for h, v in response.headers.items()
116 if not wsgiref.util.is_hop_by_hop(h)
116 if not wsgiref.util.is_hop_by_hop(h)
117 ]
117 ]
118
118
119 # Add custom response header to indicate that this is a VCS response
120 # and which backend is used.
121 response_headers.append(('X-RhodeCode-Backend', self._backend))
122
123 # TODO: johbo: Better way to get the status including text?
119 # TODO: johbo: Better way to get the status including text?
124 status = str(response.status_code)
120 status = str(response.status_code)
125 start_response(status, response_headers)
121 start_response(status, response_headers)
126 return _maybe_stream(response)
122 return _maybe_stream(response)
127
123
128
124
129 def _maybe_stream(response):
125 def _maybe_stream(response):
130 """
126 """
131 Try to generate chunks from the response if it is chunked.
127 Try to generate chunks from the response if it is chunked.
132 """
128 """
133 if _is_chunked(response):
129 if _is_chunked(response):
134 return response.raw.read_chunked()
130 return response.raw.read_chunked()
135 else:
131 else:
136 return [response.content]
132 return [response.content]
137
133
138
134
139 def _is_chunked(response):
135 def _is_chunked(response):
140 return response.headers.get('Transfer-Encoding', '') == 'chunked'
136 return response.headers.get('Transfer-Encoding', '') == 'chunked'
@@ -1,104 +1,100 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2012-2016 RhodeCode GmbH
3 # Copyright (C) 2012-2016 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 Utility to call a WSGI app wrapped in a WSGIAppCaller object.
22 Utility to call a WSGI app wrapped in a WSGIAppCaller object.
23 """
23 """
24
24
25 import logging
25 import logging
26
26
27 from Pyro4.errors import ConnectionClosedError
27 from Pyro4.errors import ConnectionClosedError
28
28
29
29
30 log = logging.getLogger(__name__)
30 log = logging.getLogger(__name__)
31
31
32
32
33 def _get_clean_environ(environ):
33 def _get_clean_environ(environ):
34 """Return a copy of the WSGI environment without wsgi.* keys.
34 """Return a copy of the WSGI environment without wsgi.* keys.
35
35
36 It also omits any non-string values.
36 It also omits any non-string values.
37
37
38 :param environ: WSGI environment to clean
38 :param environ: WSGI environment to clean
39 :type environ: dict
39 :type environ: dict
40
40
41 :returns: WSGI environment to pass to WSGIAppCaller.handle.
41 :returns: WSGI environment to pass to WSGIAppCaller.handle.
42 :rtype: dict
42 :rtype: dict
43 """
43 """
44 clean_environ = dict(
44 clean_environ = dict(
45 (k, v) for k, v in environ.iteritems()
45 (k, v) for k, v in environ.iteritems()
46 if type(v) == str and type(k) == str and not k.startswith('wsgi.')
46 if type(v) == str and type(k) == str and not k.startswith('wsgi.')
47 )
47 )
48
48
49 return clean_environ
49 return clean_environ
50
50
51
51
52 # pylint: disable=too-few-public-methods
52 # pylint: disable=too-few-public-methods
53 class RemoteAppCaller(object):
53 class RemoteAppCaller(object):
54 """Create and calls a remote WSGI app using the given factory.
54 """Create and calls a remote WSGI app using the given factory.
55
55
56 It first cleans the environment, so as to reduce the data transferred.
56 It first cleans the environment, so as to reduce the data transferred.
57 """
57 """
58
58
59 def __init__(self, remote_wsgi, backend, *args, **kwargs):
59 def __init__(self, remote_wsgi, backend, *args, **kwargs):
60 """
60 """
61 :param remote_wsgi: The remote wsgi object that creates a
61 :param remote_wsgi: The remote wsgi object that creates a
62 WSGIAppCaller. This object
62 WSGIAppCaller. This object
63 has to have a handle method, with the signature:
63 has to have a handle method, with the signature:
64 handle(environ, start_response, *args, **kwargs)
64 handle(environ, start_response, *args, **kwargs)
65 :param backend: Key (str) of the SCM backend that is in use.
65 :param backend: Key (str) of the SCM backend that is in use.
66 :param args: args to be passed to the app creation
66 :param args: args to be passed to the app creation
67 :param kwargs: kwargs to be passed to the app creation
67 :param kwargs: kwargs to be passed to the app creation
68 """
68 """
69 self._remote_wsgi = remote_wsgi
69 self._remote_wsgi = remote_wsgi
70 self._backend = backend
70 self._backend = backend
71 self._args = args
71 self._args = args
72 self._kwargs = kwargs
72 self._kwargs = kwargs
73
73
74 def __call__(self, environ, start_response):
74 def __call__(self, environ, start_response):
75 """
75 """
76 :param environ: WSGI environment with which the app will be run
76 :param environ: WSGI environment with which the app will be run
77 :type environ: dict
77 :type environ: dict
78 :param start_response: callable of WSGI protocol
78 :param start_response: callable of WSGI protocol
79 :type start_response: callable
79 :type start_response: callable
80
80
81 :returns: an iterable with the data returned by the app
81 :returns: an iterable with the data returned by the app
82 :rtype: iterable<str>
82 :rtype: iterable<str>
83 """
83 """
84 log.debug("Forwarding WSGI request via proxy %s", self._remote_wsgi)
84 log.debug("Forwarding WSGI request via proxy %s", self._remote_wsgi)
85 input_data = environ['wsgi.input'].read()
85 input_data = environ['wsgi.input'].read()
86 clean_environ = _get_clean_environ(environ)
86 clean_environ = _get_clean_environ(environ)
87
87
88 try:
88 try:
89 data, status, headers = self._remote_wsgi.handle(
89 data, status, headers = self._remote_wsgi.handle(
90 clean_environ, input_data, *self._args, **self._kwargs)
90 clean_environ, input_data, *self._args, **self._kwargs)
91 except ConnectionClosedError:
91 except ConnectionClosedError:
92 log.debug('Remote Pyro Server ConnectionClosedError')
92 log.debug('Remote Pyro Server ConnectionClosedError')
93 self._remote_wsgi._pyroReconnect(tries=15)
93 self._remote_wsgi._pyroReconnect(tries=15)
94 data, status, headers = self._remote_wsgi.handle(
94 data, status, headers = self._remote_wsgi.handle(
95 clean_environ, input_data, *self._args, **self._kwargs)
95 clean_environ, input_data, *self._args, **self._kwargs)
96
96
97 # Add custom response header to indicate that this is a VCS response
98 # and which backend is used.
99 headers.append(('X-RhodeCode-Backend', self._backend))
100
101 log.debug("Got result from proxy, returning to WSGI container")
97 log.debug("Got result from proxy, returning to WSGI container")
102 start_response(status, headers)
98 start_response(status, headers)
103
99
104 return data
100 return data
@@ -1,203 +1,201 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2010-2016 RhodeCode GmbH
3 # Copyright (C) 2010-2016 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 from StringIO import StringIO
21 from StringIO import StringIO
22
22
23 import pytest
23 import pytest
24 from mock import patch, Mock
24 from mock import patch, Mock
25
25
26 import rhodecode
26 import rhodecode
27 from rhodecode.lib.middleware.simplesvn import SimpleSvn, SimpleSvnApp
27 from rhodecode.lib.middleware.simplesvn import SimpleSvn, SimpleSvnApp
28
28
29
29
30 class TestSimpleSvn(object):
30 class TestSimpleSvn(object):
31 @pytest.fixture(autouse=True)
31 @pytest.fixture(autouse=True)
32 def simple_svn(self, pylonsapp):
32 def simple_svn(self, pylonsapp):
33 self.app = SimpleSvn(
33 self.app = SimpleSvn(
34 application='None',
34 application='None',
35 config={'auth_ret_code': '',
35 config={'auth_ret_code': '',
36 'base_path': rhodecode.CONFIG['base_path']},
36 'base_path': rhodecode.CONFIG['base_path']},
37 registry=None)
37 registry=None)
38
38
39 def test_get_config(self):
39 def test_get_config(self):
40 extras = {'foo': 'FOO', 'bar': 'BAR'}
40 extras = {'foo': 'FOO', 'bar': 'BAR'}
41 config = self.app._create_config(extras, repo_name='test-repo')
41 config = self.app._create_config(extras, repo_name='test-repo')
42 assert config == extras
42 assert config == extras
43
43
44 @pytest.mark.parametrize(
44 @pytest.mark.parametrize(
45 'method', ['OPTIONS', 'PROPFIND', 'GET', 'REPORT'])
45 'method', ['OPTIONS', 'PROPFIND', 'GET', 'REPORT'])
46 def test_get_action_returns_pull(self, method):
46 def test_get_action_returns_pull(self, method):
47 environment = {'REQUEST_METHOD': method}
47 environment = {'REQUEST_METHOD': method}
48 action = self.app._get_action(environment)
48 action = self.app._get_action(environment)
49 assert action == 'pull'
49 assert action == 'pull'
50
50
51 @pytest.mark.parametrize(
51 @pytest.mark.parametrize(
52 'method', [
52 'method', [
53 'MKACTIVITY', 'PROPPATCH', 'PUT', 'CHECKOUT', 'MKCOL', 'MOVE',
53 'MKACTIVITY', 'PROPPATCH', 'PUT', 'CHECKOUT', 'MKCOL', 'MOVE',
54 'COPY', 'DELETE', 'LOCK', 'UNLOCK', 'MERGE'
54 'COPY', 'DELETE', 'LOCK', 'UNLOCK', 'MERGE'
55 ])
55 ])
56 def test_get_action_returns_push(self, method):
56 def test_get_action_returns_push(self, method):
57 environment = {'REQUEST_METHOD': method}
57 environment = {'REQUEST_METHOD': method}
58 action = self.app._get_action(environment)
58 action = self.app._get_action(environment)
59 assert action == 'push'
59 assert action == 'push'
60
60
61 @pytest.mark.parametrize(
61 @pytest.mark.parametrize(
62 'path, expected_name', [
62 'path, expected_name', [
63 ('/hello-svn', 'hello-svn'),
63 ('/hello-svn', 'hello-svn'),
64 ('/hello-svn/', 'hello-svn'),
64 ('/hello-svn/', 'hello-svn'),
65 ('/group/hello-svn/', 'group/hello-svn'),
65 ('/group/hello-svn/', 'group/hello-svn'),
66 ('/group/hello-svn/!svn/vcc/default', 'group/hello-svn'),
66 ('/group/hello-svn/!svn/vcc/default', 'group/hello-svn'),
67 ])
67 ])
68 def test_get_repository_name(self, path, expected_name):
68 def test_get_repository_name(self, path, expected_name):
69 environment = {'PATH_INFO': path}
69 environment = {'PATH_INFO': path}
70 name = self.app._get_repository_name(environment)
70 name = self.app._get_repository_name(environment)
71 assert name == expected_name
71 assert name == expected_name
72
72
73 def test_get_repository_name_subfolder(self, backend_svn):
73 def test_get_repository_name_subfolder(self, backend_svn):
74 repo = backend_svn.repo
74 repo = backend_svn.repo
75 environment = {
75 environment = {
76 'PATH_INFO': '/{}/path/with/subfolders'.format(repo.repo_name)}
76 'PATH_INFO': '/{}/path/with/subfolders'.format(repo.repo_name)}
77 name = self.app._get_repository_name(environment)
77 name = self.app._get_repository_name(environment)
78 assert name == repo.repo_name
78 assert name == repo.repo_name
79
79
80 def test_create_wsgi_app(self):
80 def test_create_wsgi_app(self):
81 with patch.object(SimpleSvn, '_is_svn_enabled') as mock_method:
81 with patch.object(SimpleSvn, '_is_svn_enabled') as mock_method:
82 mock_method.return_value = False
82 mock_method.return_value = False
83 with patch('rhodecode.lib.middleware.simplesvn.DisabledSimpleSvnApp') as (
83 with patch('rhodecode.lib.middleware.simplesvn.DisabledSimpleSvnApp') as (
84 wsgi_app_mock):
84 wsgi_app_mock):
85 config = Mock()
85 config = Mock()
86 wsgi_app = self.app._create_wsgi_app(
86 wsgi_app = self.app._create_wsgi_app(
87 repo_path='', repo_name='', config=config)
87 repo_path='', repo_name='', config=config)
88
88
89 wsgi_app_mock.assert_called_once_with(config)
89 wsgi_app_mock.assert_called_once_with(config)
90 assert wsgi_app == wsgi_app_mock()
90 assert wsgi_app == wsgi_app_mock()
91
91
92 def test_create_wsgi_app_when_enabled(self):
92 def test_create_wsgi_app_when_enabled(self):
93 with patch.object(SimpleSvn, '_is_svn_enabled') as mock_method:
93 with patch.object(SimpleSvn, '_is_svn_enabled') as mock_method:
94 mock_method.return_value = True
94 mock_method.return_value = True
95 with patch('rhodecode.lib.middleware.simplesvn.SimpleSvnApp') as (
95 with patch('rhodecode.lib.middleware.simplesvn.SimpleSvnApp') as (
96 wsgi_app_mock):
96 wsgi_app_mock):
97 config = Mock()
97 config = Mock()
98 wsgi_app = self.app._create_wsgi_app(
98 wsgi_app = self.app._create_wsgi_app(
99 repo_path='', repo_name='', config=config)
99 repo_path='', repo_name='', config=config)
100
100
101 wsgi_app_mock.assert_called_once_with(config)
101 wsgi_app_mock.assert_called_once_with(config)
102 assert wsgi_app == wsgi_app_mock()
102 assert wsgi_app == wsgi_app_mock()
103
103
104
104
105
105
106 class TestSimpleSvnApp(object):
106 class TestSimpleSvnApp(object):
107 data = '<xml></xml>'
107 data = '<xml></xml>'
108 path = '/group/my-repo'
108 path = '/group/my-repo'
109 wsgi_input = StringIO(data)
109 wsgi_input = StringIO(data)
110 environment = {
110 environment = {
111 'HTTP_DAV': (
111 'HTTP_DAV': (
112 'http://subversion.tigris.org/xmlns/dav/svn/depth,'
112 'http://subversion.tigris.org/xmlns/dav/svn/depth,'
113 ' http://subversion.tigris.org/xmlns/dav/svn/mergeinfo'),
113 ' http://subversion.tigris.org/xmlns/dav/svn/mergeinfo'),
114 'HTTP_USER_AGENT': 'SVN/1.8.11 (x86_64-linux) serf/1.3.8',
114 'HTTP_USER_AGENT': 'SVN/1.8.11 (x86_64-linux) serf/1.3.8',
115 'REQUEST_METHOD': 'OPTIONS',
115 'REQUEST_METHOD': 'OPTIONS',
116 'PATH_INFO': path,
116 'PATH_INFO': path,
117 'wsgi.input': wsgi_input,
117 'wsgi.input': wsgi_input,
118 'CONTENT_TYPE': 'text/xml',
118 'CONTENT_TYPE': 'text/xml',
119 'CONTENT_LENGTH': '130'
119 'CONTENT_LENGTH': '130'
120 }
120 }
121
121
122 def setup_method(self, method):
122 def setup_method(self, method):
123 self.host = 'http://localhost/'
123 self.host = 'http://localhost/'
124 self.app = SimpleSvnApp(
124 self.app = SimpleSvnApp(
125 config={'subversion_http_server_url': self.host})
125 config={'subversion_http_server_url': self.host})
126
126
127 def test_get_request_headers_with_content_type(self):
127 def test_get_request_headers_with_content_type(self):
128 expected_headers = {
128 expected_headers = {
129 'Dav': self.environment['HTTP_DAV'],
129 'Dav': self.environment['HTTP_DAV'],
130 'User-Agent': self.environment['HTTP_USER_AGENT'],
130 'User-Agent': self.environment['HTTP_USER_AGENT'],
131 'Content-Type': self.environment['CONTENT_TYPE'],
131 'Content-Type': self.environment['CONTENT_TYPE'],
132 'Content-Length': self.environment['CONTENT_LENGTH']
132 'Content-Length': self.environment['CONTENT_LENGTH']
133 }
133 }
134 headers = self.app._get_request_headers(self.environment)
134 headers = self.app._get_request_headers(self.environment)
135 assert headers == expected_headers
135 assert headers == expected_headers
136
136
137 def test_get_request_headers_without_content_type(self):
137 def test_get_request_headers_without_content_type(self):
138 environment = self.environment.copy()
138 environment = self.environment.copy()
139 environment.pop('CONTENT_TYPE')
139 environment.pop('CONTENT_TYPE')
140 expected_headers = {
140 expected_headers = {
141 'Dav': environment['HTTP_DAV'],
141 'Dav': environment['HTTP_DAV'],
142 'Content-Length': self.environment['CONTENT_LENGTH'],
142 'Content-Length': self.environment['CONTENT_LENGTH'],
143 'User-Agent': environment['HTTP_USER_AGENT'],
143 'User-Agent': environment['HTTP_USER_AGENT'],
144 }
144 }
145 request_headers = self.app._get_request_headers(environment)
145 request_headers = self.app._get_request_headers(environment)
146 assert request_headers == expected_headers
146 assert request_headers == expected_headers
147
147
148 def test_get_response_headers(self):
148 def test_get_response_headers(self):
149 headers = {
149 headers = {
150 'Connection': 'keep-alive',
150 'Connection': 'keep-alive',
151 'Keep-Alive': 'timeout=5, max=100',
151 'Keep-Alive': 'timeout=5, max=100',
152 'Transfer-Encoding': 'chunked',
152 'Transfer-Encoding': 'chunked',
153 'Content-Encoding': 'gzip',
153 'Content-Encoding': 'gzip',
154 'MS-Author-Via': 'DAV',
154 'MS-Author-Via': 'DAV',
155 'SVN-Supported-Posts': 'create-txn-with-props'
155 'SVN-Supported-Posts': 'create-txn-with-props'
156 }
156 }
157 expected_headers = [
157 expected_headers = [
158 ('MS-Author-Via', 'DAV'),
158 ('MS-Author-Via', 'DAV'),
159 ('SVN-Supported-Posts', 'create-txn-with-props'),
159 ('SVN-Supported-Posts', 'create-txn-with-props'),
160 ('X-RhodeCode-Backend', 'svn'),
161 ]
160 ]
162 response_headers = self.app._get_response_headers(headers)
161 response_headers = self.app._get_response_headers(headers)
163 assert sorted(response_headers) == sorted(expected_headers)
162 assert sorted(response_headers) == sorted(expected_headers)
164
163
165 def test_get_url(self):
164 def test_get_url(self):
166 url = self.app._get_url(self.path)
165 url = self.app._get_url(self.path)
167 expected_url = '{}{}'.format(self.host.strip('/'), self.path)
166 expected_url = '{}{}'.format(self.host.strip('/'), self.path)
168 assert url == expected_url
167 assert url == expected_url
169
168
170 def test_call(self):
169 def test_call(self):
171 start_response = Mock()
170 start_response = Mock()
172 response_mock = Mock()
171 response_mock = Mock()
173 response_mock.headers = {
172 response_mock.headers = {
174 'Content-Encoding': 'gzip',
173 'Content-Encoding': 'gzip',
175 'MS-Author-Via': 'DAV',
174 'MS-Author-Via': 'DAV',
176 'SVN-Supported-Posts': 'create-txn-with-props'
175 'SVN-Supported-Posts': 'create-txn-with-props'
177 }
176 }
178 response_mock.status_code = 200
177 response_mock.status_code = 200
179 response_mock.reason = 'OK'
178 response_mock.reason = 'OK'
180 with patch('rhodecode.lib.middleware.simplesvn.requests.request') as (
179 with patch('rhodecode.lib.middleware.simplesvn.requests.request') as (
181 request_mock):
180 request_mock):
182 request_mock.return_value = response_mock
181 request_mock.return_value = response_mock
183 self.app(self.environment, start_response)
182 self.app(self.environment, start_response)
184
183
185 expected_url = '{}{}'.format(self.host.strip('/'), self.path)
184 expected_url = '{}{}'.format(self.host.strip('/'), self.path)
186 expected_request_headers = {
185 expected_request_headers = {
187 'Dav': self.environment['HTTP_DAV'],
186 'Dav': self.environment['HTTP_DAV'],
188 'User-Agent': self.environment['HTTP_USER_AGENT'],
187 'User-Agent': self.environment['HTTP_USER_AGENT'],
189 'Content-Type': self.environment['CONTENT_TYPE'],
188 'Content-Type': self.environment['CONTENT_TYPE'],
190 'Content-Length': self.environment['CONTENT_LENGTH']
189 'Content-Length': self.environment['CONTENT_LENGTH']
191 }
190 }
192 expected_response_headers = [
191 expected_response_headers = [
193 ('SVN-Supported-Posts', 'create-txn-with-props'),
192 ('SVN-Supported-Posts', 'create-txn-with-props'),
194 ('MS-Author-Via', 'DAV'),
193 ('MS-Author-Via', 'DAV'),
195 ('X-RhodeCode-Backend', 'svn'),
196 ]
194 ]
197 request_mock.assert_called_once_with(
195 request_mock.assert_called_once_with(
198 self.environment['REQUEST_METHOD'], expected_url,
196 self.environment['REQUEST_METHOD'], expected_url,
199 data=self.data, headers=expected_request_headers)
197 data=self.data, headers=expected_request_headers)
200 response_mock.iter_content.assert_called_once_with(chunk_size=1024)
198 response_mock.iter_content.assert_called_once_with(chunk_size=1024)
201 args, _ = start_response.call_args
199 args, _ = start_response.call_args
202 assert args[0] == '200 OK'
200 assert args[0] == '200 OK'
203 assert sorted(args[1]) == sorted(expected_response_headers)
201 assert sorted(args[1]) == sorted(expected_response_headers)
General Comments 0
You need to be logged in to leave comments. Login now