##// END OF EJS Templates
vcs: Do not pass the backend key into the scm app instances....
Martin Bornhold -
r951:8ff080b9 default
parent child Browse files
Show More
@@ -1,85 +1,85 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 """
21 """
22 SimpleGit middleware for handling git protocol request (push/clone etc.)
22 SimpleGit middleware for handling git protocol request (push/clone etc.)
23 It's implemented with basic auth function
23 It's implemented with basic auth function
24 """
24 """
25 import re
25 import re
26 import logging
26 import logging
27 import urlparse
27 import urlparse
28
28
29 import rhodecode
29 import rhodecode
30 from rhodecode.lib import utils2
30 from rhodecode.lib import utils2
31 from rhodecode.lib.middleware import simplevcs
31 from rhodecode.lib.middleware import simplevcs
32
32
33 log = logging.getLogger(__name__)
33 log = logging.getLogger(__name__)
34
34
35
35
36 GIT_PROTO_PAT = re.compile(
36 GIT_PROTO_PAT = re.compile(
37 r'^/(.+)/(info/refs|git-upload-pack|git-receive-pack)')
37 r'^/(.+)/(info/refs|git-upload-pack|git-receive-pack)')
38
38
39
39
40 class SimpleGit(simplevcs.SimpleVCS):
40 class SimpleGit(simplevcs.SimpleVCS):
41
41
42 SCM = 'git'
42 SCM = 'git'
43
43
44 def _get_repository_name(self, environ):
44 def _get_repository_name(self, environ):
45 """
45 """
46 Gets repository name out of PATH_INFO header
46 Gets repository name out of PATH_INFO header
47
47
48 :param environ: environ where PATH_INFO is stored
48 :param environ: environ where PATH_INFO is stored
49 """
49 """
50 repo_name = GIT_PROTO_PAT.match(environ['PATH_INFO']).group(1)
50 repo_name = GIT_PROTO_PAT.match(environ['PATH_INFO']).group(1)
51 return repo_name
51 return repo_name
52
52
53 _ACTION_MAPPING = {
53 _ACTION_MAPPING = {
54 'git-receive-pack': 'push',
54 'git-receive-pack': 'push',
55 'git-upload-pack': 'pull',
55 'git-upload-pack': 'pull',
56 }
56 }
57
57
58 def _get_action(self, environ):
58 def _get_action(self, environ):
59 """
59 """
60 Maps git request commands into a pull or push command.
60 Maps git request commands into a pull or push command.
61 In case of unknown/unexpected data, it returns 'pull' to be safe.
61 In case of unknown/unexpected data, it returns 'pull' to be safe.
62
62
63 :param environ:
63 :param environ:
64 """
64 """
65 path = environ['PATH_INFO']
65 path = environ['PATH_INFO']
66
66
67 if path.endswith('/info/refs'):
67 if path.endswith('/info/refs'):
68 query = urlparse.parse_qs(environ['QUERY_STRING'])
68 query = urlparse.parse_qs(environ['QUERY_STRING'])
69 service_cmd = query.get('service', [''])[0]
69 service_cmd = query.get('service', [''])[0]
70 return self._ACTION_MAPPING.get(service_cmd, 'pull')
70 return self._ACTION_MAPPING.get(service_cmd, 'pull')
71 elif path.endswith('/git-receive-pack'):
71 elif path.endswith('/git-receive-pack'):
72 return 'push'
72 return 'push'
73 elif path.endswith('/git-upload-pack'):
73 elif path.endswith('/git-upload-pack'):
74 return 'pull'
74 return 'pull'
75
75
76 return 'pull'
76 return 'pull'
77
77
78 def _create_wsgi_app(self, repo_path, repo_name, config):
78 def _create_wsgi_app(self, repo_path, repo_name, config):
79 return self.scm_app.create_git_wsgi_app(
79 return self.scm_app.create_git_wsgi_app(
80 repo_path, repo_name, config, self.SCM)
80 repo_path, repo_name, config)
81
81
82 def _create_config(self, extras, repo_name):
82 def _create_config(self, extras, repo_name):
83 extras['git_update_server_info'] = utils2.str2bool(
83 extras['git_update_server_info'] = utils2.str2bool(
84 rhodecode.CONFIG.get('git_update_server_info'))
84 rhodecode.CONFIG.get('git_update_server_info'))
85 return extras
85 return extras
@@ -1,81 +1,81 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 """
21 """
22 SimpleHG middleware for handling mercurial protocol request
22 SimpleHG middleware for handling mercurial protocol request
23 (push/clone etc.). It's implemented with basic auth function
23 (push/clone etc.). It's implemented with basic auth function
24 """
24 """
25
25
26 import logging
26 import logging
27 import urlparse
27 import urlparse
28
28
29 from rhodecode.lib import utils
29 from rhodecode.lib import utils
30 from rhodecode.lib.ext_json import json
30 from rhodecode.lib.ext_json import json
31 from rhodecode.lib.middleware import simplevcs
31 from rhodecode.lib.middleware import simplevcs
32
32
33 log = logging.getLogger(__name__)
33 log = logging.getLogger(__name__)
34
34
35
35
36 class SimpleHg(simplevcs.SimpleVCS):
36 class SimpleHg(simplevcs.SimpleVCS):
37
37
38 SCM = 'hg'
38 SCM = 'hg'
39
39
40 def _get_repository_name(self, environ):
40 def _get_repository_name(self, environ):
41 """
41 """
42 Gets repository name out of PATH_INFO header
42 Gets repository name out of PATH_INFO header
43
43
44 :param environ: environ where PATH_INFO is stored
44 :param environ: environ where PATH_INFO is stored
45 """
45 """
46 return environ['PATH_INFO'].strip('/')
46 return environ['PATH_INFO'].strip('/')
47
47
48 _ACTION_MAPPING = {
48 _ACTION_MAPPING = {
49 'changegroup': 'pull',
49 'changegroup': 'pull',
50 'changegroupsubset': 'pull',
50 'changegroupsubset': 'pull',
51 'getbundle': 'pull',
51 'getbundle': 'pull',
52 'stream_out': 'pull',
52 'stream_out': 'pull',
53 'listkeys': 'pull',
53 'listkeys': 'pull',
54 'unbundle': 'push',
54 'unbundle': 'push',
55 'pushkey': 'push',
55 'pushkey': 'push',
56 }
56 }
57
57
58 def _get_action(self, environ):
58 def _get_action(self, environ):
59 """
59 """
60 Maps mercurial request commands into a pull or push command.
60 Maps mercurial request commands into a pull or push command.
61 In case of unknown/unexpected data, it returns 'pull' to be safe.
61 In case of unknown/unexpected data, it returns 'pull' to be safe.
62
62
63 :param environ:
63 :param environ:
64 """
64 """
65 query = urlparse.parse_qs(environ['QUERY_STRING'],
65 query = urlparse.parse_qs(environ['QUERY_STRING'],
66 keep_blank_values=True)
66 keep_blank_values=True)
67 if 'cmd' in query:
67 if 'cmd' in query:
68 cmd = query['cmd'][0]
68 cmd = query['cmd'][0]
69 return self._ACTION_MAPPING.get(cmd, 'pull')
69 return self._ACTION_MAPPING.get(cmd, 'pull')
70
70
71 return 'pull'
71 return 'pull'
72
72
73 def _create_wsgi_app(self, repo_path, repo_name, config):
73 def _create_wsgi_app(self, repo_path, repo_name, config):
74 return self.scm_app.create_hg_wsgi_app(
74 return self.scm_app.create_hg_wsgi_app(
75 repo_path, repo_name, config, self.SCM)
75 repo_path, repo_name, config)
76
76
77 def _create_config(self, extras, repo_name):
77 def _create_config(self, extras, repo_name):
78 config = utils.make_db_config(repo=repo_name)
78 config = utils.make_db_config(repo=repo_name)
79 config.set('rhodecode', 'RC_SCM_DATA', json.dumps(extras))
79 config.set('rhodecode', 'RC_SCM_DATA', json.dumps(extras))
80
80
81 return config.serialize()
81 return config.serialize()
@@ -1,63 +1,63 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 Equivalent of rhodecode.lib.middleware.scm_app but using remote apps.
22 Equivalent of rhodecode.lib.middleware.scm_app but using remote apps.
23 """
23 """
24
24
25 import logging
25 import logging
26
26
27 from rhodecode.lib.middleware.utils import wsgi_app_caller_client
27 from rhodecode.lib.middleware.utils import wsgi_app_caller_client
28
28
29
29
30 log = logging.getLogger(__name__)
30 log = logging.getLogger(__name__)
31
31
32
32
33 HG_REMOTE_WSGI = None
33 HG_REMOTE_WSGI = None
34 GIT_REMOTE_WSGI = None
34 GIT_REMOTE_WSGI = None
35
35
36
36
37 def create_git_wsgi_app(repo_path, repo_name, config, backend):
37 def create_git_wsgi_app(repo_path, repo_name, config):
38 """
38 """
39 Return a WSGI app backed by a remote app to handle Git.
39 Return a WSGI app backed by a remote app to handle Git.
40
40
41 config is a dictionary holding the extras.
41 config is a dictionary holding the extras.
42 """
42 """
43 factory = GIT_REMOTE_WSGI
43 factory = GIT_REMOTE_WSGI
44 if not factory:
44 if not factory:
45 log.error('Pyro server has not been initialized yet')
45 log.error('Pyro server has not been initialized yet')
46
46
47 return wsgi_app_caller_client.RemoteAppCaller(
47 return wsgi_app_caller_client.RemoteAppCaller(
48 factory, backend, repo_path, repo_name, config)
48 factory, repo_path, repo_name, config)
49
49
50
50
51 def create_hg_wsgi_app(repo_path, repo_name, config, backend):
51 def create_hg_wsgi_app(repo_path, repo_name, config):
52 """
52 """
53 Return a WSGI app backed by a remote app to handle Mercurial.
53 Return a WSGI app backed by a remote app to handle Mercurial.
54
54
55 config is a list of 3-item tuples representing a ConfigObject (it is the
55 config is a list of 3-item tuples representing a ConfigObject (it is the
56 serialized version of the config object).
56 serialized version of the config object).
57 """
57 """
58 factory = HG_REMOTE_WSGI
58 factory = HG_REMOTE_WSGI
59 if not factory:
59 if not factory:
60 log.error('Pyro server has not been initialized yet')
60 log.error('Pyro server has not been initialized yet')
61
61
62 return wsgi_app_caller_client.RemoteAppCaller(
62 return wsgi_app_caller_client.RemoteAppCaller(
63 factory, backend, repo_path, repo_name, config)
63 factory, repo_path, repo_name, config)
@@ -1,136 +1,135 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):
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)
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):
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)
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):
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
79 log.debug(
78 log.debug(
80 "Creating VcsHttpProxy for repo %s, url %s",
79 "Creating VcsHttpProxy for repo %s, url %s",
81 repo_name, url)
80 repo_name, url)
82
81
83 def __call__(self, environ, start_response):
82 def __call__(self, environ, start_response):
84 status = '200 OK'
83 status = '200 OK'
85
84
86 config = msgpack.packb(self._config)
85 config = msgpack.packb(self._config)
87 request = webob.request.Request(environ)
86 request = webob.request.Request(environ)
88 request_headers = request.headers
87 request_headers = request.headers
89 request_headers.update({
88 request_headers.update({
90 # TODO: johbo: Remove this, rely on URL path only
89 # TODO: johbo: Remove this, rely on URL path only
91 'X-RC-Repo-Name': self._repo_name,
90 'X-RC-Repo-Name': self._repo_name,
92 'X-RC-Repo-Path': self._repo_path,
91 'X-RC-Repo-Path': self._repo_path,
93 'X-RC-Path-Info': environ['PATH_INFO'],
92 'X-RC-Path-Info': environ['PATH_INFO'],
94 # TODO: johbo: Avoid encoding and put this into payload?
93 # TODO: johbo: Avoid encoding and put this into payload?
95 'X-RC-Repo-Config': base64.b64encode(config),
94 'X-RC-Repo-Config': base64.b64encode(config),
96 })
95 })
97
96
98 data = environ['wsgi.input'].read()
97 data = environ['wsgi.input'].read()
99 method = environ['REQUEST_METHOD']
98 method = environ['REQUEST_METHOD']
100
99
101 # Preserve the query string
100 # Preserve the query string
102 url = self._url
101 url = self._url
103 url = urlparse.urljoin(url, self._repo_name)
102 url = urlparse.urljoin(url, self._repo_name)
104 if environ.get('QUERY_STRING'):
103 if environ.get('QUERY_STRING'):
105 url += '?' + environ['QUERY_STRING']
104 url += '?' + environ['QUERY_STRING']
106
105
107 response = session.request(
106 response = session.request(
108 method, url,
107 method, url,
109 data=data,
108 data=data,
110 headers=request_headers,
109 headers=request_headers,
111 stream=True)
110 stream=True)
112
111
113 # Preserve the headers of the response, except hop_by_hop ones
112 # Preserve the headers of the response, except hop_by_hop ones
114 response_headers = [
113 response_headers = [
115 (h, v) for h, v in response.headers.items()
114 (h, v) for h, v in response.headers.items()
116 if not wsgiref.util.is_hop_by_hop(h)
115 if not wsgiref.util.is_hop_by_hop(h)
117 ]
116 ]
118
117
119 # TODO: johbo: Better way to get the status including text?
118 # TODO: johbo: Better way to get the status including text?
120 status = str(response.status_code)
119 status = str(response.status_code)
121 start_response(status, response_headers)
120 start_response(status, response_headers)
122 return _maybe_stream(response)
121 return _maybe_stream(response)
123
122
124
123
125 def _maybe_stream(response):
124 def _maybe_stream(response):
126 """
125 """
127 Try to generate chunks from the response if it is chunked.
126 Try to generate chunks from the response if it is chunked.
128 """
127 """
129 if _is_chunked(response):
128 if _is_chunked(response):
130 return response.raw.read_chunked()
129 return response.raw.read_chunked()
131 else:
130 else:
132 return [response.content]
131 return [response.content]
133
132
134
133
135 def _is_chunked(response):
134 def _is_chunked(response):
136 return response.headers.get('Transfer-Encoding', '') == 'chunked'
135 return response.headers.get('Transfer-Encoding', '') == 'chunked'
@@ -1,100 +1,98 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, *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.
66 :param args: args to be passed to the app creation
65 :param args: args to be passed to the app creation
67 :param kwargs: kwargs to be passed to the app creation
66 :param kwargs: kwargs to be passed to the app creation
68 """
67 """
69 self._remote_wsgi = remote_wsgi
68 self._remote_wsgi = remote_wsgi
70 self._backend = backend
71 self._args = args
69 self._args = args
72 self._kwargs = kwargs
70 self._kwargs = kwargs
73
71
74 def __call__(self, environ, start_response):
72 def __call__(self, environ, start_response):
75 """
73 """
76 :param environ: WSGI environment with which the app will be run
74 :param environ: WSGI environment with which the app will be run
77 :type environ: dict
75 :type environ: dict
78 :param start_response: callable of WSGI protocol
76 :param start_response: callable of WSGI protocol
79 :type start_response: callable
77 :type start_response: callable
80
78
81 :returns: an iterable with the data returned by the app
79 :returns: an iterable with the data returned by the app
82 :rtype: iterable<str>
80 :rtype: iterable<str>
83 """
81 """
84 log.debug("Forwarding WSGI request via proxy %s", self._remote_wsgi)
82 log.debug("Forwarding WSGI request via proxy %s", self._remote_wsgi)
85 input_data = environ['wsgi.input'].read()
83 input_data = environ['wsgi.input'].read()
86 clean_environ = _get_clean_environ(environ)
84 clean_environ = _get_clean_environ(environ)
87
85
88 try:
86 try:
89 data, status, headers = self._remote_wsgi.handle(
87 data, status, headers = self._remote_wsgi.handle(
90 clean_environ, input_data, *self._args, **self._kwargs)
88 clean_environ, input_data, *self._args, **self._kwargs)
91 except ConnectionClosedError:
89 except ConnectionClosedError:
92 log.debug('Remote Pyro Server ConnectionClosedError')
90 log.debug('Remote Pyro Server ConnectionClosedError')
93 self._remote_wsgi._pyroReconnect(tries=15)
91 self._remote_wsgi._pyroReconnect(tries=15)
94 data, status, headers = self._remote_wsgi.handle(
92 data, status, headers = self._remote_wsgi.handle(
95 clean_environ, input_data, *self._args, **self._kwargs)
93 clean_environ, input_data, *self._args, **self._kwargs)
96
94
97 log.debug("Got result from proxy, returning to WSGI container")
95 log.debug("Got result from proxy, returning to WSGI container")
98 start_response(status, headers)
96 start_response(status, headers)
99
97
100 return data
98 return data
@@ -1,41 +1,41 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 """
21 """
22 Provides a mock implementation of the scm_app module.
22 Provides a mock implementation of the scm_app module.
23
23
24 It resembles the same API as :mod:`rhodecode.lib.middleware.utils.scm_app`
24 It resembles the same API as :mod:`rhodecode.lib.middleware.utils.scm_app`
25 for testing purposes.
25 for testing purposes.
26 """
26 """
27 import mock
27 import mock
28
28
29
29
30 def create_git_wsgi_app(repo_path, repo_name, config, backend):
30 def create_git_wsgi_app(repo_path, repo_name, config):
31 return mock_git_wsgi
31 return mock_git_wsgi
32
32
33
33
34 def create_hg_wsgi_app(repo_path, repo_name, config, backend):
34 def create_hg_wsgi_app(repo_path, repo_name, config):
35 return mock_hg_wsgi
35 return mock_hg_wsgi
36
36
37
37
38 # Utilities to ease testing
38 # Utilities to ease testing
39
39
40 mock_hg_wsgi = mock.Mock()
40 mock_hg_wsgi = mock.Mock()
41 mock_git_wsgi = mock.Mock()
41 mock_git_wsgi = mock.Mock()
@@ -1,131 +1,130 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2016-2016 RhodeCode GmbH
3 # Copyright (C) 2016-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 mock
21 import mock
22 import Pyro4
22 import Pyro4
23 import pytest
23 import pytest
24 import webtest
24 import webtest
25
25
26 from rhodecode.lib.middleware.utils import scm_app_http, scm_app
26 from rhodecode.lib.middleware.utils import scm_app_http, scm_app
27 from rhodecode.lib.middleware.simplegit import SimpleGit
28 from rhodecode.lib.vcs.conf import settings
27 from rhodecode.lib.vcs.conf import settings
29
28
30
29
31 def vcs_http_app(vcsserver_http_echo_app):
30 def vcs_http_app(vcsserver_http_echo_app):
32 """
31 """
33 VcsHttpProxy wrapped in WebTest.
32 VcsHttpProxy wrapped in WebTest.
34 """
33 """
35 git_url = vcsserver_http_echo_app.http_url + 'stream/git/'
34 git_url = vcsserver_http_echo_app.http_url + 'stream/git/'
36 vcs_http_proxy = scm_app_http.VcsHttpProxy(
35 vcs_http_proxy = scm_app_http.VcsHttpProxy(
37 git_url, 'stub_path', 'stub_name', None, 'stub_backend')
36 git_url, 'stub_path', 'stub_name', None)
38 app = webtest.TestApp(vcs_http_proxy)
37 app = webtest.TestApp(vcs_http_proxy)
39 return app
38 return app
40
39
41
40
42 @pytest.fixture(scope='module')
41 @pytest.fixture(scope='module')
43 def vcsserver_http_echo_app(request, vcsserver_factory):
42 def vcsserver_http_echo_app(request, vcsserver_factory):
44 """
43 """
45 A running VCSServer with the EchoApp activated via HTTP.
44 A running VCSServer with the EchoApp activated via HTTP.
46 """
45 """
47 vcsserver = vcsserver_factory(
46 vcsserver = vcsserver_factory(
48 request=request,
47 request=request,
49 use_http=True,
48 use_http=True,
50 overrides=[{'app:main': {'dev.use_echo_app': 'true'}}])
49 overrides=[{'app:main': {'dev.use_echo_app': 'true'}}])
51 return vcsserver
50 return vcsserver
52
51
53
52
54 @pytest.fixture(scope='session')
53 @pytest.fixture(scope='session')
55 def data():
54 def data():
56 one_kb = "x" * 1024
55 one_kb = "x" * 1024
57 return one_kb * 1024 * 10
56 return one_kb * 1024 * 10
58
57
59
58
60 def test_reuse_app_no_data(repeat, vcsserver_http_echo_app):
59 def test_reuse_app_no_data(repeat, vcsserver_http_echo_app):
61 app = vcs_http_app(vcsserver_http_echo_app)
60 app = vcs_http_app(vcsserver_http_echo_app)
62 for x in xrange(repeat / 10):
61 for x in xrange(repeat / 10):
63 response = app.post('/')
62 response = app.post('/')
64 assert response.status_code == 200
63 assert response.status_code == 200
65
64
66
65
67 def test_reuse_app_with_data(data, repeat, vcsserver_http_echo_app):
66 def test_reuse_app_with_data(data, repeat, vcsserver_http_echo_app):
68 app = vcs_http_app(vcsserver_http_echo_app)
67 app = vcs_http_app(vcsserver_http_echo_app)
69 for x in xrange(repeat / 10):
68 for x in xrange(repeat / 10):
70 response = app.post('/', params=data)
69 response = app.post('/', params=data)
71 assert response.status_code == 200
70 assert response.status_code == 200
72
71
73
72
74 def test_create_app_per_request_no_data(repeat, vcsserver_http_echo_app):
73 def test_create_app_per_request_no_data(repeat, vcsserver_http_echo_app):
75 for x in xrange(repeat / 10):
74 for x in xrange(repeat / 10):
76 app = vcs_http_app(vcsserver_http_echo_app)
75 app = vcs_http_app(vcsserver_http_echo_app)
77 response = app.post('/')
76 response = app.post('/')
78 assert response.status_code == 200
77 assert response.status_code == 200
79
78
80
79
81 def test_create_app_per_request_with_data(
80 def test_create_app_per_request_with_data(
82 data, repeat, vcsserver_http_echo_app):
81 data, repeat, vcsserver_http_echo_app):
83 for x in xrange(repeat / 10):
82 for x in xrange(repeat / 10):
84 app = vcs_http_app(vcsserver_http_echo_app)
83 app = vcs_http_app(vcsserver_http_echo_app)
85 response = app.post('/', params=data)
84 response = app.post('/', params=data)
86 assert response.status_code == 200
85 assert response.status_code == 200
87
86
88
87
89 @pytest.fixture(scope='module')
88 @pytest.fixture(scope='module')
90 def vcsserver_pyro_echo_app(request, vcsserver_factory):
89 def vcsserver_pyro_echo_app(request, vcsserver_factory):
91 """
90 """
92 A running VCSServer with the EchoApp activated via Pyro4.
91 A running VCSServer with the EchoApp activated via Pyro4.
93 """
92 """
94 vcsserver = vcsserver_factory(
93 vcsserver = vcsserver_factory(
95 request=request,
94 request=request,
96 use_http=False,
95 use_http=False,
97 overrides=[{'DEFAULT': {'dev.use_echo_app': 'true'}}])
96 overrides=[{'DEFAULT': {'dev.use_echo_app': 'true'}}])
98 return vcsserver
97 return vcsserver
99
98
100
99
101 def vcs_pyro4_app(vcsserver_pyro_echo_app):
100 def vcs_pyro4_app(vcsserver_pyro_echo_app):
102 """
101 """
103 Pyro4 based Vcs proxy wrapped in WebTest
102 Pyro4 based Vcs proxy wrapped in WebTest
104 """
103 """
105 stub_config = {
104 stub_config = {
106 'git_update_server_info': 'stub',
105 'git_update_server_info': 'stub',
107 }
106 }
108 server_and_port = vcsserver_pyro_echo_app.server_and_port
107 server_and_port = vcsserver_pyro_echo_app.server_and_port
109 GIT_REMOTE_WSGI = Pyro4.Proxy(
108 GIT_REMOTE_WSGI = Pyro4.Proxy(
110 settings.pyro_remote(
109 settings.pyro_remote(
111 settings.PYRO_GIT_REMOTE_WSGI, server_and_port))
110 settings.PYRO_GIT_REMOTE_WSGI, server_and_port))
112 with mock.patch('rhodecode.lib.middleware.utils.scm_app.GIT_REMOTE_WSGI',
111 with mock.patch('rhodecode.lib.middleware.utils.scm_app.GIT_REMOTE_WSGI',
113 GIT_REMOTE_WSGI):
112 GIT_REMOTE_WSGI):
114 pyro4_app = scm_app.create_git_wsgi_app(
113 pyro4_app = scm_app.create_git_wsgi_app(
115 'stub_path', 'stub_name', stub_config, SimpleGit.SCM)
114 'stub_path', 'stub_name', stub_config)
116 app = webtest.TestApp(pyro4_app)
115 app = webtest.TestApp(pyro4_app)
117 return app
116 return app
118
117
119
118
120 def test_pyro4_no_data(repeat, pylonsapp, vcsserver_pyro_echo_app):
119 def test_pyro4_no_data(repeat, pylonsapp, vcsserver_pyro_echo_app):
121 for x in xrange(repeat / 10):
120 for x in xrange(repeat / 10):
122 app = vcs_pyro4_app(vcsserver_pyro_echo_app)
121 app = vcs_pyro4_app(vcsserver_pyro_echo_app)
123 response = app.post('/')
122 response = app.post('/')
124 assert response.status_code == 200
123 assert response.status_code == 200
125
124
126
125
127 def test_pyro4_with_data(repeat, pylonsapp, vcsserver_pyro_echo_app, data):
126 def test_pyro4_with_data(repeat, pylonsapp, vcsserver_pyro_echo_app, data):
128 for x in xrange(repeat / 10):
127 for x in xrange(repeat / 10):
129 app = vcs_pyro4_app(vcsserver_pyro_echo_app)
128 app = vcs_pyro4_app(vcsserver_pyro_echo_app)
130 response = app.post('/', params=data)
129 response = app.post('/', params=data)
131 assert response.status_code == 200
130 assert response.status_code == 200
@@ -1,136 +1,136 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2016-2016 RhodeCode GmbH
3 # Copyright (C) 2016-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 Checking the chunked data transfer via HTTP
22 Checking the chunked data transfer via HTTP
23 """
23 """
24
24
25 import os
25 import os
26 import time
26 import time
27 import subprocess
27 import subprocess
28
28
29 import pytest
29 import pytest
30 import requests
30 import requests
31
31
32 from rhodecode.lib.middleware.utils import scm_app_http
32 from rhodecode.lib.middleware.utils import scm_app_http
33 from rhodecode.tests.utils import wait_for_url
33 from rhodecode.tests.utils import wait_for_url
34
34
35
35
36 def test_does_chunked_end_to_end_transfer(scm_app):
36 def test_does_chunked_end_to_end_transfer(scm_app):
37 response = requests.post(scm_app, data='', stream=True)
37 response = requests.post(scm_app, data='', stream=True)
38 assert response.headers['Transfer-Encoding'] == 'chunked'
38 assert response.headers['Transfer-Encoding'] == 'chunked'
39 times = [time.time() for chunk in response.raw.read_chunked()]
39 times = [time.time() for chunk in response.raw.read_chunked()]
40 assert times[1] - times[0] > 0.1, "Chunks arrived at the same time"
40 assert times[1] - times[0] > 0.1, "Chunks arrived at the same time"
41
41
42
42
43 @pytest.fixture
43 @pytest.fixture
44 def echo_app_chunking(request, available_port_factory):
44 def echo_app_chunking(request, available_port_factory):
45 """
45 """
46 Run the EchoApp via Waitress in a subprocess.
46 Run the EchoApp via Waitress in a subprocess.
47
47
48 Return the URL endpoint to reach the app.
48 Return the URL endpoint to reach the app.
49 """
49 """
50 port = available_port_factory()
50 port = available_port_factory()
51 command = (
51 command = (
52 'waitress-serve --send-bytes 1 --port {port} --call '
52 'waitress-serve --send-bytes 1 --port {port} --call '
53 'rhodecode.tests.lib.middleware.utils.test_scm_app_http_chunking'
53 'rhodecode.tests.lib.middleware.utils.test_scm_app_http_chunking'
54 ':create_echo_app')
54 ':create_echo_app')
55 command = command.format(port=port)
55 command = command.format(port=port)
56 proc = subprocess.Popen(command.split(' '), bufsize=0)
56 proc = subprocess.Popen(command.split(' '), bufsize=0)
57 echo_app_url = 'http://localhost:' + str(port)
57 echo_app_url = 'http://localhost:' + str(port)
58
58
59 @request.addfinalizer
59 @request.addfinalizer
60 def stop_echo_app():
60 def stop_echo_app():
61 proc.kill()
61 proc.kill()
62
62
63 return echo_app_url
63 return echo_app_url
64
64
65
65
66 @pytest.fixture
66 @pytest.fixture
67 def scm_app(request, available_port_factory, echo_app_chunking):
67 def scm_app(request, available_port_factory, echo_app_chunking):
68 """
68 """
69 Run the scm_app in Waitress.
69 Run the scm_app in Waitress.
70
70
71 Returns the URL endpoint where this app can be reached.
71 Returns the URL endpoint where this app can be reached.
72 """
72 """
73 port = available_port_factory()
73 port = available_port_factory()
74 command = (
74 command = (
75 'waitress-serve --send-bytes 1 --port {port} --call '
75 'waitress-serve --send-bytes 1 --port {port} --call '
76 'rhodecode.tests.lib.middleware.utils.test_scm_app_http_chunking'
76 'rhodecode.tests.lib.middleware.utils.test_scm_app_http_chunking'
77 ':create_scm_app')
77 ':create_scm_app')
78 command = command.format(port=port)
78 command = command.format(port=port)
79 env = os.environ.copy()
79 env = os.environ.copy()
80 env["RC_ECHO_URL"] = echo_app_chunking
80 env["RC_ECHO_URL"] = echo_app_chunking
81 proc = subprocess.Popen(command.split(' '), bufsize=0, env=env)
81 proc = subprocess.Popen(command.split(' '), bufsize=0, env=env)
82 scm_app_url = 'http://localhost:' + str(port)
82 scm_app_url = 'http://localhost:' + str(port)
83 wait_for_url(scm_app_url)
83 wait_for_url(scm_app_url)
84
84
85 @request.addfinalizer
85 @request.addfinalizer
86 def stop_echo_app():
86 def stop_echo_app():
87 proc.kill()
87 proc.kill()
88
88
89 return scm_app_url
89 return scm_app_url
90
90
91
91
92 class EchoApp(object):
92 class EchoApp(object):
93 """
93 """
94 Stub WSGI application which returns a chunked response to every request.
94 Stub WSGI application which returns a chunked response to every request.
95 """
95 """
96
96
97 def __init__(self, repo_path, repo_name, config):
97 def __init__(self, repo_path, repo_name, config):
98 self._repo_path = repo_path
98 self._repo_path = repo_path
99
99
100 def __call__(self, environ, start_response):
100 def __call__(self, environ, start_response):
101 environ['wsgi.input'].read()
101 environ['wsgi.input'].read()
102 status = '200 OK'
102 status = '200 OK'
103 headers = []
103 headers = []
104 start_response(status, headers)
104 start_response(status, headers)
105 return result_generator()
105 return result_generator()
106
106
107
107
108 def result_generator():
108 def result_generator():
109 """
109 """
110 Simulate chunked results.
110 Simulate chunked results.
111
111
112 The intended usage is to simulate a chunked response as we would get it
112 The intended usage is to simulate a chunked response as we would get it
113 out of a vcs operation during a call to "hg clone".
113 out of a vcs operation during a call to "hg clone".
114 """
114 """
115 yield 'waiting 2 seconds'
115 yield 'waiting 2 seconds'
116 # Wait long enough so that the first chunk can go out
116 # Wait long enough so that the first chunk can go out
117 time.sleep(2)
117 time.sleep(2)
118 yield 'final chunk'
118 yield 'final chunk'
119 # Another small wait, otherwise they go together
119 # Another small wait, otherwise they go together
120 time.sleep(0.1)
120 time.sleep(0.1)
121
121
122
122
123 def create_echo_app():
123 def create_echo_app():
124 """
124 """
125 Create EchoApp filled with stub data.
125 Create EchoApp filled with stub data.
126 """
126 """
127 return EchoApp('stub_path', 'repo_name', {})
127 return EchoApp('stub_path', 'repo_name', {})
128
128
129
129
130 def create_scm_app():
130 def create_scm_app():
131 """
131 """
132 Create a scm_app hooked up to speak to EchoApp.
132 Create a scm_app hooked up to speak to EchoApp.
133 """
133 """
134 echo_app_url = os.environ["RC_ECHO_URL"]
134 echo_app_url = os.environ["RC_ECHO_URL"]
135 return scm_app_http.VcsHttpProxy(
135 return scm_app_http.VcsHttpProxy(
136 echo_app_url, 'stub_path', 'stub_name', None, 'stub_backend')
136 echo_app_url, 'stub_path', 'stub_name', None)
@@ -1,102 +1,100 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 webtest
21 import webtest
22
22
23 from rhodecode.lib.middleware.utils import wsgi_app_caller_client
23 from rhodecode.lib.middleware.utils import wsgi_app_caller_client
24
24
25 # pylint: disable=protected-access,too-many-public-methods
25 # pylint: disable=protected-access,too-many-public-methods
26
26
27
27
28 BASE_ENVIRON = {
28 BASE_ENVIRON = {
29 'REQUEST_METHOD': 'GET',
29 'REQUEST_METHOD': 'GET',
30 'SERVER_NAME': 'localhost',
30 'SERVER_NAME': 'localhost',
31 'SERVER_PORT': '80',
31 'SERVER_PORT': '80',
32 'SCRIPT_NAME': '',
32 'SCRIPT_NAME': '',
33 'PATH_INFO': '/',
33 'PATH_INFO': '/',
34 'QUERY_STRING': '',
34 'QUERY_STRING': '',
35 'foo.bool_var': True,
35 'foo.bool_var': True,
36 'foo.str_var': 'True',
36 'foo.str_var': 'True',
37 'wsgi.foo': True,
37 'wsgi.foo': True,
38 # Some non string values. The validator expects to get an iterable as
38 # Some non string values. The validator expects to get an iterable as
39 # value.
39 # value.
40 (42,): '42',
40 (42,): '42',
41 (True,): 'False',
41 (True,): 'False',
42 }
42 }
43
43
44
44
45 def assert_all_values_are_str(environ):
45 def assert_all_values_are_str(environ):
46 """Checks that all values of a dict are str."""
46 """Checks that all values of a dict are str."""
47 for key, value in environ.iteritems():
47 for key, value in environ.iteritems():
48 assert isinstance(value, str), (
48 assert isinstance(value, str), (
49 "Value for key %s: has type %s but 'str' was expected. Value: %s" %
49 "Value for key %s: has type %s but 'str' was expected. Value: %s" %
50 (key, type(value), repr(value)))
50 (key, type(value), repr(value)))
51
51
52
52
53 def assert_all_keys_are_str(environ):
53 def assert_all_keys_are_str(environ):
54 """Checks that all keys of a dict are str."""
54 """Checks that all keys of a dict are str."""
55 for key, value in environ.iteritems():
55 for key, value in environ.iteritems():
56 assert isinstance(value, str), (
56 assert isinstance(value, str), (
57 "Key %s: has type %s but 'str' was expected. " %
57 "Key %s: has type %s but 'str' was expected. " %
58 (repr(key), type(key)))
58 (repr(key), type(key)))
59
59
60
60
61 def assert_no_prefix_in_keys(environ, prefix):
61 def assert_no_prefix_in_keys(environ, prefix):
62 """Checks that no key of the dict starts with the prefix."""
62 """Checks that no key of the dict starts with the prefix."""
63 for key in environ:
63 for key in environ:
64 assert not key.startswith(prefix), 'Key %s should not be present' % key
64 assert not key.startswith(prefix), 'Key %s should not be present' % key
65
65
66
66
67 def test_get_environ():
67 def test_get_environ():
68 clean_environ = wsgi_app_caller_client._get_clean_environ(BASE_ENVIRON)
68 clean_environ = wsgi_app_caller_client._get_clean_environ(BASE_ENVIRON)
69
69
70 assert len(clean_environ) == 7
70 assert len(clean_environ) == 7
71 assert_no_prefix_in_keys(clean_environ, 'wsgi.')
71 assert_no_prefix_in_keys(clean_environ, 'wsgi.')
72 assert_all_keys_are_str(clean_environ)
72 assert_all_keys_are_str(clean_environ)
73 assert_all_values_are_str(clean_environ)
73 assert_all_values_are_str(clean_environ)
74
74
75
75
76 def test_remote_app_caller():
76 def test_remote_app_caller():
77
77
78 class RemoteAppCallerMock(object):
78 class RemoteAppCallerMock(object):
79
79
80 def handle(self, environ, input_data, arg1, arg2,
80 def handle(self, environ, input_data, arg1, arg2,
81 arg3=None, arg4=None, arg5=None):
81 arg3=None, arg4=None, arg5=None):
82 assert ((arg1, arg2, arg3, arg4, arg5) ==
82 assert ((arg1, arg2, arg3, arg4, arg5) ==
83 ('a1', 'a2', 'a3', 'a4', None))
83 ('a1', 'a2', 'a3', 'a4', None))
84 # Note: RemoteAppCaller is expected to return a tuple like the
84 # Note: RemoteAppCaller is expected to return a tuple like the
85 # following one
85 # following one
86 return (['content'], '200 OK', [('Content-Type', 'text/plain')])
86 return (['content'], '200 OK', [('Content-Type', 'text/plain')])
87
87
88 wrapper_app = wsgi_app_caller_client.RemoteAppCaller(
88 wrapper_app = wsgi_app_caller_client.RemoteAppCaller(
89 RemoteAppCallerMock(), 'dummy-backend',
89 RemoteAppCallerMock(), 'a1', 'a2', arg3='a3', arg4='a4')
90 'a1', 'a2', arg3='a3', arg4='a4')
91
90
92 test_app = webtest.TestApp(wrapper_app)
91 test_app = webtest.TestApp(wrapper_app)
93
92
94 response = test_app.get('/path')
93 response = test_app.get('/path')
95
94
96 assert response.status == '200 OK'
95 assert response.status == '200 OK'
97 assert sorted(response.headers.items()) == sorted([
96 assert sorted(response.headers.items()) == sorted([
98 ('X-RhodeCode-Backend', 'dummy-backend'),
99 ('Content-Type', 'text/plain'),
97 ('Content-Type', 'text/plain'),
100 ('Content-Length', '7'),
98 ('Content-Length', '7'),
101 ])
99 ])
102 assert response.body == 'content'
100 assert response.body == 'content'
General Comments 0
You need to be logged in to leave comments. Login now