##// END OF EJS Templates
subversion: Detect requests also based on magic path.
johbo -
r437:28802ace default
parent child Browse files
Show More
@@ -1,156 +1,160 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 gzip
21 import gzip
22 import shutil
22 import shutil
23 import logging
23 import logging
24 import tempfile
24 import tempfile
25 import urlparse
25 import urlparse
26
26
27 import rhodecode
27 import rhodecode
28 from rhodecode.lib.middleware.appenlight import wrap_in_appenlight_if_enabled
28 from rhodecode.lib.middleware.appenlight import wrap_in_appenlight_if_enabled
29 from rhodecode.lib.middleware.simplegit import SimpleGit, GIT_PROTO_PAT
29 from rhodecode.lib.middleware.simplegit import SimpleGit, GIT_PROTO_PAT
30 from rhodecode.lib.middleware.simplehg import SimpleHg
30 from rhodecode.lib.middleware.simplehg import SimpleHg
31 from rhodecode.lib.middleware.simplesvn import SimpleSvn
31 from rhodecode.lib.middleware.simplesvn import SimpleSvn
32
32
33
33
34 log = logging.getLogger(__name__)
34 log = logging.getLogger(__name__)
35
35
36
36
37 def is_git(environ):
37 def is_git(environ):
38 """
38 """
39 Returns True if requests should be handled by GIT wsgi middleware
39 Returns True if requests should be handled by GIT wsgi middleware
40 """
40 """
41 is_git_path = GIT_PROTO_PAT.match(environ['PATH_INFO'])
41 is_git_path = GIT_PROTO_PAT.match(environ['PATH_INFO'])
42 log.debug(
42 log.debug(
43 'request path: `%s` detected as GIT PROTOCOL %s', environ['PATH_INFO'],
43 'request path: `%s` detected as GIT PROTOCOL %s', environ['PATH_INFO'],
44 is_git_path is not None)
44 is_git_path is not None)
45
45
46 return is_git_path
46 return is_git_path
47
47
48
48
49 def is_hg(environ):
49 def is_hg(environ):
50 """
50 """
51 Returns True if requests target is mercurial server - header
51 Returns True if requests target is mercurial server - header
52 ``HTTP_ACCEPT`` of such request would start with ``application/mercurial``.
52 ``HTTP_ACCEPT`` of such request would start with ``application/mercurial``.
53 """
53 """
54 is_hg_path = False
54 is_hg_path = False
55
55
56 http_accept = environ.get('HTTP_ACCEPT')
56 http_accept = environ.get('HTTP_ACCEPT')
57
57
58 if http_accept and http_accept.startswith('application/mercurial'):
58 if http_accept and http_accept.startswith('application/mercurial'):
59 query = urlparse.parse_qs(environ['QUERY_STRING'])
59 query = urlparse.parse_qs(environ['QUERY_STRING'])
60 if 'cmd' in query:
60 if 'cmd' in query:
61 is_hg_path = True
61 is_hg_path = True
62
62
63 log.debug(
63 log.debug(
64 'request path: `%s` detected as HG PROTOCOL %s', environ['PATH_INFO'],
64 'request path: `%s` detected as HG PROTOCOL %s', environ['PATH_INFO'],
65 is_hg_path)
65 is_hg_path)
66
66
67 return is_hg_path
67 return is_hg_path
68
68
69
69
70 def is_svn(environ):
70 def is_svn(environ):
71 """
71 """
72 Returns True if requests target is Subversion server
72 Returns True if requests target is Subversion server
73 """
73 """
74 http_dav = environ.get('HTTP_DAV', '')
74 http_dav = environ.get('HTTP_DAV', '')
75 is_svn_path = 'subversion' in http_dav
75 magic_path_segment = rhodecode.CONFIG.get(
76 'rhodecode_subversion_magic_path', '/!svn')
77 is_svn_path = (
78 'subversion' in http_dav or
79 magic_path_segment in environ['PATH_INFO'])
76 log.debug(
80 log.debug(
77 'request path: `%s` detected as SVN PROTOCOL %s', environ['PATH_INFO'],
81 'request path: `%s` detected as SVN PROTOCOL %s', environ['PATH_INFO'],
78 is_svn_path)
82 is_svn_path)
79
83
80 return is_svn_path
84 return is_svn_path
81
85
82
86
83 class GunzipMiddleware(object):
87 class GunzipMiddleware(object):
84 """
88 """
85 WSGI middleware that unzips gzip-encoded requests before
89 WSGI middleware that unzips gzip-encoded requests before
86 passing on to the underlying application.
90 passing on to the underlying application.
87 """
91 """
88
92
89 def __init__(self, application):
93 def __init__(self, application):
90 self.app = application
94 self.app = application
91
95
92 def __call__(self, environ, start_response):
96 def __call__(self, environ, start_response):
93 accepts_encoding_header = environ.get('HTTP_CONTENT_ENCODING', b'')
97 accepts_encoding_header = environ.get('HTTP_CONTENT_ENCODING', b'')
94
98
95 if b'gzip' in accepts_encoding_header:
99 if b'gzip' in accepts_encoding_header:
96 log.debug('gzip detected, now running gunzip wrapper')
100 log.debug('gzip detected, now running gunzip wrapper')
97 wsgi_input = environ['wsgi.input']
101 wsgi_input = environ['wsgi.input']
98
102
99 if not hasattr(environ['wsgi.input'], 'seek'):
103 if not hasattr(environ['wsgi.input'], 'seek'):
100 # The gzip implementation in the standard library of Python 2.x
104 # The gzip implementation in the standard library of Python 2.x
101 # requires the '.seek()' and '.tell()' methods to be available
105 # requires the '.seek()' and '.tell()' methods to be available
102 # on the input stream. Read the data into a temporary file to
106 # on the input stream. Read the data into a temporary file to
103 # work around this limitation.
107 # work around this limitation.
104
108
105 wsgi_input = tempfile.SpooledTemporaryFile(64 * 1024 * 1024)
109 wsgi_input = tempfile.SpooledTemporaryFile(64 * 1024 * 1024)
106 shutil.copyfileobj(environ['wsgi.input'], wsgi_input)
110 shutil.copyfileobj(environ['wsgi.input'], wsgi_input)
107 wsgi_input.seek(0)
111 wsgi_input.seek(0)
108
112
109 environ['wsgi.input'] = gzip.GzipFile(fileobj=wsgi_input, mode='r')
113 environ['wsgi.input'] = gzip.GzipFile(fileobj=wsgi_input, mode='r')
110 # since we "Ungzipped" the content we say now it's no longer gzip
114 # since we "Ungzipped" the content we say now it's no longer gzip
111 # content encoding
115 # content encoding
112 del environ['HTTP_CONTENT_ENCODING']
116 del environ['HTTP_CONTENT_ENCODING']
113
117
114 # content length has changes ? or i'm not sure
118 # content length has changes ? or i'm not sure
115 if 'CONTENT_LENGTH' in environ:
119 if 'CONTENT_LENGTH' in environ:
116 del environ['CONTENT_LENGTH']
120 del environ['CONTENT_LENGTH']
117 else:
121 else:
118 log.debug('content not gzipped, gzipMiddleware passing '
122 log.debug('content not gzipped, gzipMiddleware passing '
119 'request further')
123 'request further')
120 return self.app(environ, start_response)
124 return self.app(environ, start_response)
121
125
122
126
123 class VCSMiddleware(object):
127 class VCSMiddleware(object):
124
128
125 def __init__(self, app, config, appenlight_client):
129 def __init__(self, app, config, appenlight_client):
126 self.application = app
130 self.application = app
127 self.config = config
131 self.config = config
128 self.appenlight_client = appenlight_client
132 self.appenlight_client = appenlight_client
129
133
130 def _get_handler_app(self, environ):
134 def _get_handler_app(self, environ):
131 app = None
135 app = None
132 if is_hg(environ):
136 if is_hg(environ):
133 app = SimpleHg(self.application, self.config)
137 app = SimpleHg(self.application, self.config)
134
138
135 if is_git(environ):
139 if is_git(environ):
136 app = SimpleGit(self.application, self.config)
140 app = SimpleGit(self.application, self.config)
137
141
138 proxy_svn = rhodecode.CONFIG.get(
142 proxy_svn = rhodecode.CONFIG.get(
139 'rhodecode_proxy_subversion_http_requests', False)
143 'rhodecode_proxy_subversion_http_requests', False)
140 if proxy_svn and is_svn(environ):
144 if proxy_svn and is_svn(environ):
141 app = SimpleSvn(self.application, self.config)
145 app = SimpleSvn(self.application, self.config)
142
146
143 if app:
147 if app:
144 app = GunzipMiddleware(app)
148 app = GunzipMiddleware(app)
145 app, _ = wrap_in_appenlight_if_enabled(
149 app, _ = wrap_in_appenlight_if_enabled(
146 app, self.config, self.appenlight_client)
150 app, self.config, self.appenlight_client)
147
151
148 return app
152 return app
149
153
150 def __call__(self, environ, start_response):
154 def __call__(self, environ, start_response):
151 # check if we handle one of interesting protocols ?
155 # check if we handle one of interesting protocols ?
152 vcs_handler = self._get_handler_app(environ)
156 vcs_handler = self._get_handler_app(environ)
153 if vcs_handler:
157 if vcs_handler:
154 return vcs_handler(environ, start_response)
158 return vcs_handler(environ, start_response)
155
159
156 return self.application(environ, start_response)
160 return self.application(environ, start_response)
@@ -1,114 +1,134 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 mock import patch, Mock
21 from mock import patch, Mock
22
22
23 import rhodecode
23 import rhodecode
24 from rhodecode.lib.middleware import vcs
24 from rhodecode.lib.middleware import vcs
25
25
26
26
27 def test_is_hg():
27 def test_is_hg():
28 environ = {
28 environ = {
29 'PATH_INFO': 'rhodecode-dev',
29 'PATH_INFO': 'rhodecode-dev',
30 'QUERY_STRING': 'cmd=changegroup',
30 'QUERY_STRING': 'cmd=changegroup',
31 'HTTP_ACCEPT': 'application/mercurial'
31 'HTTP_ACCEPT': 'application/mercurial'
32 }
32 }
33 assert vcs.is_hg(environ)
33 assert vcs.is_hg(environ)
34
34
35
35
36 def test_is_hg_no_cmd():
36 def test_is_hg_no_cmd():
37 environ = {
37 environ = {
38 'PATH_INFO': 'rhodecode-dev',
38 'PATH_INFO': 'rhodecode-dev',
39 'QUERY_STRING': '',
39 'QUERY_STRING': '',
40 'HTTP_ACCEPT': 'application/mercurial'
40 'HTTP_ACCEPT': 'application/mercurial'
41 }
41 }
42 assert not vcs.is_hg(environ)
42 assert not vcs.is_hg(environ)
43
43
44
44
45 def test_is_hg_empty_cmd():
45 def test_is_hg_empty_cmd():
46 environ = {
46 environ = {
47 'PATH_INFO': 'rhodecode-dev',
47 'PATH_INFO': 'rhodecode-dev',
48 'QUERY_STRING': 'cmd=',
48 'QUERY_STRING': 'cmd=',
49 'HTTP_ACCEPT': 'application/mercurial'
49 'HTTP_ACCEPT': 'application/mercurial'
50 }
50 }
51 assert not vcs.is_hg(environ)
51 assert not vcs.is_hg(environ)
52
52
53
53
54 def test_is_svn_returns_true_if_subversion_is_in_a_dav_header():
54 def test_is_svn_returns_true_if_subversion_is_in_a_dav_header():
55 environ = {
55 environ = {
56 'PATH_INFO': 'rhodecode-dev',
56 'PATH_INFO': 'rhodecode-dev',
57 'HTTP_DAV': 'http://subversion.tigris.org/xmlns/dav/svn/log-revprops'
57 'HTTP_DAV': 'http://subversion.tigris.org/xmlns/dav/svn/log-revprops'
58 }
58 }
59 assert vcs.is_svn(environ) is True
59 assert vcs.is_svn(environ) is True
60
60
61
61
62 def test_is_svn_returns_false_if_subversion_is_not_in_a_dav_header():
62 def test_is_svn_returns_false_if_subversion_is_not_in_a_dav_header():
63 environ = {
63 environ = {
64 'PATH_INFO': 'rhodecode-dev',
64 'PATH_INFO': 'rhodecode-dev',
65 'HTTP_DAV': 'http://stuff.tigris.org/xmlns/dav/svn/log-revprops'
65 'HTTP_DAV': 'http://stuff.tigris.org/xmlns/dav/svn/log-revprops'
66 }
66 }
67 assert vcs.is_svn(environ) is False
67 assert vcs.is_svn(environ) is False
68
68
69
69
70 def test_is_svn_returns_false_if_no_dav_header():
70 def test_is_svn_returns_false_if_no_dav_header():
71 environ = {
71 environ = {
72 'PATH_INFO': 'rhodecode-dev',
72 'PATH_INFO': 'rhodecode-dev',
73 }
73 }
74 assert vcs.is_svn(environ) is False
74 assert vcs.is_svn(environ) is False
75
75
76
76
77 def test_is_svn_returns_true_if_magic_path_segment():
78 environ = {
79 'PATH_INFO': '/stub-repository/!svn/rev/4',
80 }
81 assert vcs.is_svn(environ)
82
83
84 def test_is_svn_allows_to_configure_the_magic_path(monkeypatch):
85 """
86 This is intended as a fallback in case someone has configured his
87 Subversion server with a different magic path segment.
88 """
89 monkeypatch.setitem(
90 rhodecode.CONFIG, 'rhodecode_subversion_magic_path', '/!my-magic')
91 environ = {
92 'PATH_INFO': '/stub-repository/!my-magic/rev/4',
93 }
94 assert vcs.is_svn(environ)
95
96
77 class TestVCSMiddleware(object):
97 class TestVCSMiddleware(object):
78 def test_get_handler_app_retuns_svn_app_when_proxy_enabled(self):
98 def test_get_handler_app_retuns_svn_app_when_proxy_enabled(self):
79 environ = {
99 environ = {
80 'PATH_INFO': 'rhodecode-dev',
100 'PATH_INFO': 'rhodecode-dev',
81 'HTTP_DAV': 'http://subversion.tigris.org/xmlns/dav/svn/log'
101 'HTTP_DAV': 'http://subversion.tigris.org/xmlns/dav/svn/log'
82 }
102 }
83 app = Mock()
103 app = Mock()
84 config = Mock()
104 config = Mock()
85 middleware = vcs.VCSMiddleware(
105 middleware = vcs.VCSMiddleware(
86 app, config=config, appenlight_client=None)
106 app, config=config, appenlight_client=None)
87 snv_patch = patch('rhodecode.lib.middleware.vcs.SimpleSvn')
107 snv_patch = patch('rhodecode.lib.middleware.vcs.SimpleSvn')
88 settings_patch = patch.dict(
108 settings_patch = patch.dict(
89 rhodecode.CONFIG,
109 rhodecode.CONFIG,
90 {'rhodecode_proxy_subversion_http_requests': True})
110 {'rhodecode_proxy_subversion_http_requests': True})
91 with snv_patch as svn_mock, settings_patch:
111 with snv_patch as svn_mock, settings_patch:
92 svn_mock.return_value = None
112 svn_mock.return_value = None
93 middleware._get_handler_app(environ)
113 middleware._get_handler_app(environ)
94
114
95 svn_mock.assert_called_once_with(app, config)
115 svn_mock.assert_called_once_with(app, config)
96
116
97 def test_get_handler_app_retuns_no_svn_app_when_proxy_disabled(self):
117 def test_get_handler_app_retuns_no_svn_app_when_proxy_disabled(self):
98 environ = {
118 environ = {
99 'PATH_INFO': 'rhodecode-dev',
119 'PATH_INFO': 'rhodecode-dev',
100 'HTTP_DAV': 'http://subversion.tigris.org/xmlns/dav/svn/log'
120 'HTTP_DAV': 'http://subversion.tigris.org/xmlns/dav/svn/log'
101 }
121 }
102 app = Mock()
122 app = Mock()
103 config = Mock()
123 config = Mock()
104 middleware = vcs.VCSMiddleware(
124 middleware = vcs.VCSMiddleware(
105 app, config=config, appenlight_client=None)
125 app, config=config, appenlight_client=None)
106 snv_patch = patch('rhodecode.lib.middleware.vcs.SimpleSvn')
126 snv_patch = patch('rhodecode.lib.middleware.vcs.SimpleSvn')
107 settings_patch = patch.dict(
127 settings_patch = patch.dict(
108 rhodecode.CONFIG,
128 rhodecode.CONFIG,
109 {'rhodecode_proxy_subversion_http_requests': False})
129 {'rhodecode_proxy_subversion_http_requests': False})
110 with snv_patch as svn_mock, settings_patch:
130 with snv_patch as svn_mock, settings_patch:
111 app = middleware._get_handler_app(environ)
131 app = middleware._get_handler_app(environ)
112
132
113 assert svn_mock.call_count == 0
133 assert svn_mock.call_count == 0
114 assert app is None
134 assert app is None
General Comments 0
You need to be logged in to leave comments. Login now