##// END OF EJS Templates
mercurial: protocol security updates....
marcink -
r2724:7a057a98 default
parent child Browse files
Show More
@@ -1,81 +1,152 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2010-2018 RhodeCode GmbH
3 # Copyright (C) 2010-2018 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 import urllib
28
29
29 from rhodecode.lib import utils
30 from rhodecode.lib import utils
30 from rhodecode.lib.ext_json import json
31 from rhodecode.lib.ext_json import json
31 from rhodecode.lib.middleware import simplevcs
32 from rhodecode.lib.middleware import simplevcs
32
33
33 log = logging.getLogger(__name__)
34 log = logging.getLogger(__name__)
34
35
35
36
36 class SimpleHg(simplevcs.SimpleVCS):
37 class SimpleHg(simplevcs.SimpleVCS):
37
38
38 SCM = 'hg'
39 SCM = 'hg'
39
40
40 def _get_repository_name(self, environ):
41 def _get_repository_name(self, environ):
41 """
42 """
42 Gets repository name out of PATH_INFO header
43 Gets repository name out of PATH_INFO header
43
44
44 :param environ: environ where PATH_INFO is stored
45 :param environ: environ where PATH_INFO is stored
45 """
46 """
46 return environ['PATH_INFO'].strip('/')
47 return environ['PATH_INFO'].strip('/')
47
48
48 _ACTION_MAPPING = {
49 _ACTION_MAPPING = {
49 'changegroup': 'pull',
50 'changegroup': 'pull',
50 'changegroupsubset': 'pull',
51 'changegroupsubset': 'pull',
51 'getbundle': 'pull',
52 'getbundle': 'pull',
52 'stream_out': 'pull',
53 'stream_out': 'pull',
53 'listkeys': 'pull',
54 'listkeys': 'pull',
55 'between': 'pull',
56 'branchmap': 'pull',
57 'branches': 'pull',
58 'clonebundles': 'pull',
59 'capabilities': 'pull',
60 'debugwireargs': 'pull',
61 'heads': 'pull',
62 'lookup': 'pull',
63 'hello': 'pull',
64 'known': 'pull',
65
66 # largefiles
67 'putlfile': 'push',
68 'getlfile': 'pull',
69 'statlfile': 'pull',
70 'lheads': 'pull',
71
54 'unbundle': 'push',
72 'unbundle': 'push',
55 'pushkey': 'push',
73 'pushkey': 'push',
56 }
74 }
57
75
76 @classmethod
77 def _get_xarg_headers(cls, environ):
78 i = 1
79 chunks = [] # gather chunks stored in multiple 'hgarg_N'
80 while True:
81 head = environ.get('HTTP_X_HGARG_{}'.format(i))
82 if not head:
83 break
84 i += 1
85 chunks.append(urllib.unquote_plus(head))
86 full_arg = ''.join(chunks)
87 pref = 'cmds='
88 if full_arg.startswith(pref):
89 # strip the cmds= header defining our batch commands
90 full_arg = full_arg[len(pref):]
91 cmds = full_arg.split(';')
92 return cmds
93
94 @classmethod
95 def _get_batch_cmd(cls, environ):
96 """
97 Handle batch command send commands. Those are ';' separated commands
98 sent by batch command that server needs to execute. We need to extract
99 those, and map them to our ACTION_MAPPING to get all push/pull commands
100 specified in the batch
101 """
102 default = 'push'
103 batch_cmds = []
104 try:
105 cmds = cls._get_xarg_headers(environ)
106 for pair in cmds:
107 parts = pair.split(' ', 1)
108 if len(parts) != 2:
109 continue
110 # entry should be in a format `key ARGS`
111 cmd, args = parts
112 action = cls._ACTION_MAPPING.get(cmd, default)
113 batch_cmds.append(action)
114 except Exception:
115 log.exception('Failed to extract batch commands operations')
116
117 # in case we failed, (e.g malformed data) assume it's PUSH sub-command
118 # for safety
119 return batch_cmds or [default]
120
58 def _get_action(self, environ):
121 def _get_action(self, environ):
59 """
122 """
60 Maps mercurial request commands into a pull or push command.
123 Maps mercurial request commands into a pull or push command.
61 In case of unknown/unexpected data, it returns 'pull' to be safe.
124 In case of unknown/unexpected data, it returns 'push' to be safe.
62
125
63 :param environ:
126 :param environ:
64 """
127 """
128 default = 'push'
65 query = urlparse.parse_qs(environ['QUERY_STRING'],
129 query = urlparse.parse_qs(environ['QUERY_STRING'],
66 keep_blank_values=True)
130 keep_blank_values=True)
131
67 if 'cmd' in query:
132 if 'cmd' in query:
68 cmd = query['cmd'][0]
133 cmd = query['cmd'][0]
69 return self._ACTION_MAPPING.get(cmd, 'pull')
134 if cmd == 'batch':
135 cmds = self._get_batch_cmd(environ)
136 if 'push' in cmds:
137 return 'push'
138 else:
139 return 'pull'
140 return self._ACTION_MAPPING.get(cmd, default)
70
141
71 return 'pull'
142 return default
72
143
73 def _create_wsgi_app(self, repo_path, repo_name, config):
144 def _create_wsgi_app(self, repo_path, repo_name, config):
74 return self.scm_app.create_hg_wsgi_app(
145 return self.scm_app.create_hg_wsgi_app(
75 repo_path, repo_name, config)
146 repo_path, repo_name, config)
76
147
77 def _create_config(self, extras, repo_name):
148 def _create_config(self, extras, repo_name):
78 config = utils.make_db_config(repo=repo_name)
149 config = utils.make_db_config(repo=repo_name)
79 config.set('rhodecode', 'RC_SCM_DATA', json.dumps(extras))
150 config.set('rhodecode', 'RC_SCM_DATA', json.dumps(extras))
80
151
81 return config.serialize()
152 return config.serialize()
@@ -1,125 +1,156 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2010-2018 RhodeCode GmbH
3 # Copyright (C) 2010-2018 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 urlparse
21 import urlparse
22
22
23 import mock
23 import mock
24 import pytest
24 import pytest
25 import simplejson as json
25 import simplejson as json
26
26
27 from rhodecode.lib.vcs.backends.base import Config
27 from rhodecode.lib.vcs.backends.base import Config
28 from rhodecode.tests.lib.middleware import mock_scm_app
28 from rhodecode.tests.lib.middleware import mock_scm_app
29 import rhodecode.lib.middleware.simplehg as simplehg
29 import rhodecode.lib.middleware.simplehg as simplehg
30
30
31
31
32 def get_environ(url):
32 def get_environ(url):
33 """Construct a minimum WSGI environ based on the URL."""
33 """Construct a minimum WSGI environ based on the URL."""
34 parsed_url = urlparse.urlparse(url)
34 parsed_url = urlparse.urlparse(url)
35 environ = {
35 environ = {
36 'PATH_INFO': parsed_url.path,
36 'PATH_INFO': parsed_url.path,
37 'QUERY_STRING': parsed_url.query,
37 'QUERY_STRING': parsed_url.query,
38 }
38 }
39
39
40 return environ
40 return environ
41
41
42
42
43 @pytest.mark.parametrize(
43 @pytest.mark.parametrize(
44 'url, expected_action',
44 'url, expected_action',
45 [
45 [
46 ('/foo/bar?cmd=unbundle&key=tip', 'push'),
46 ('/foo/bar?cmd=unbundle&key=tip', 'push'),
47 ('/foo/bar?cmd=pushkey&key=tip', 'push'),
47 ('/foo/bar?cmd=pushkey&key=tip', 'push'),
48 ('/foo/bar?cmd=listkeys&key=tip', 'pull'),
48 ('/foo/bar?cmd=listkeys&key=tip', 'pull'),
49 ('/foo/bar?cmd=changegroup&key=tip', 'pull'),
49 ('/foo/bar?cmd=changegroup&key=tip', 'pull'),
50 # Edge case: unknown argument: assume pull
50 ('/foo/bar?cmd=hello', 'pull'),
51 ('/foo/bar?cmd=unknown&key=tip', 'pull'),
51 ('/foo/bar?cmd=batch', 'push'),
52 ('/foo/bar?cmd=&key=tip', 'pull'),
52 ('/foo/bar?cmd=putlfile', 'push'),
53 # Edge case: unknown argument: assume push
54 ('/foo/bar?cmd=unknown&key=tip', 'push'),
55 ('/foo/bar?cmd=&key=tip', 'push'),
53 # Edge case: not cmd argument
56 # Edge case: not cmd argument
54 ('/foo/bar?key=tip', 'pull'),
57 ('/foo/bar?key=tip', 'push'),
55 ])
58 ])
56 def test_get_action(url, expected_action, request_stub):
59 def test_get_action(url, expected_action, request_stub):
57 app = simplehg.SimpleHg(config={'auth_ret_code': '', 'base_path': ''},
60 app = simplehg.SimpleHg(config={'auth_ret_code': '', 'base_path': ''},
58 registry=request_stub.registry)
61 registry=request_stub.registry)
59 assert expected_action == app._get_action(get_environ(url))
62 assert expected_action == app._get_action(get_environ(url))
60
63
61
64
62 @pytest.mark.parametrize(
65 @pytest.mark.parametrize(
66 'environ, expected_xargs, expected_batch',
67 [
68 ({},
69 [''], ['push']),
70
71 ({'HTTP_X_HGARG_1': ''},
72 [''], ['push']),
73
74 ({'HTTP_X_HGARG_1': 'cmds=listkeys+namespace%3Dphases'},
75 ['listkeys namespace=phases'], ['pull']),
76
77 ({'HTTP_X_HGARG_1': 'cmds=pushkey+namespace%3Dbookmarks%2Ckey%3Dbm%2Cold%3D%2Cnew%3Dcb9a9f314b8b07ba71012fcdbc544b5a4d82ff5b'},
78 ['pushkey namespace=bookmarks,key=bm,old=,new=cb9a9f314b8b07ba71012fcdbc544b5a4d82ff5b'], ['push']),
79
80 ({'HTTP_X_HGARG_1': 'namespace=phases'},
81 ['namespace=phases'], ['push']),
82
83 ])
84 def test_xarg_and_batch_commands(environ, expected_xargs, expected_batch):
85 app = simplehg.SimpleHg
86
87 result = app._get_xarg_headers(environ)
88 result_batch = app._get_batch_cmd(environ)
89 assert expected_xargs == result
90 assert expected_batch == result_batch
91
92
93 @pytest.mark.parametrize(
63 'url, expected_repo_name',
94 'url, expected_repo_name',
64 [
95 [
65 ('/foo?cmd=unbundle&key=tip', 'foo'),
96 ('/foo?cmd=unbundle&key=tip', 'foo'),
66 ('/foo/bar?cmd=pushkey&key=tip', 'foo/bar'),
97 ('/foo/bar?cmd=pushkey&key=tip', 'foo/bar'),
67 ('/foo/bar/baz?cmd=listkeys&key=tip', 'foo/bar/baz'),
98 ('/foo/bar/baz?cmd=listkeys&key=tip', 'foo/bar/baz'),
68 # Repos with trailing slashes.
99 # Repos with trailing slashes.
69 ('/foo/?cmd=unbundle&key=tip', 'foo'),
100 ('/foo/?cmd=unbundle&key=tip', 'foo'),
70 ('/foo/bar/?cmd=pushkey&key=tip', 'foo/bar'),
101 ('/foo/bar/?cmd=pushkey&key=tip', 'foo/bar'),
71 ('/foo/bar/baz/?cmd=listkeys&key=tip', 'foo/bar/baz'),
102 ('/foo/bar/baz/?cmd=listkeys&key=tip', 'foo/bar/baz'),
72 ])
103 ])
73 def test_get_repository_name(url, expected_repo_name, request_stub):
104 def test_get_repository_name(url, expected_repo_name, request_stub):
74 app = simplehg.SimpleHg(config={'auth_ret_code': '', 'base_path': ''},
105 app = simplehg.SimpleHg(config={'auth_ret_code': '', 'base_path': ''},
75 registry=request_stub.registry)
106 registry=request_stub.registry)
76 assert expected_repo_name == app._get_repository_name(get_environ(url))
107 assert expected_repo_name == app._get_repository_name(get_environ(url))
77
108
78
109
79 def test_get_config(user_util, baseapp, request_stub):
110 def test_get_config(user_util, baseapp, request_stub):
80 repo = user_util.create_repo(repo_type='git')
111 repo = user_util.create_repo(repo_type='git')
81 app = simplehg.SimpleHg(config={'auth_ret_code': '', 'base_path': ''},
112 app = simplehg.SimpleHg(config={'auth_ret_code': '', 'base_path': ''},
82 registry=request_stub.registry)
113 registry=request_stub.registry)
83 extras = [('foo', 'FOO', 'bar', 'BAR')]
114 extras = [('foo', 'FOO', 'bar', 'BAR')]
84
115
85 hg_config = app._create_config(extras, repo_name=repo.repo_name)
116 hg_config = app._create_config(extras, repo_name=repo.repo_name)
86
117
87 config = simplehg.utils.make_db_config(repo=repo.repo_name)
118 config = simplehg.utils.make_db_config(repo=repo.repo_name)
88 config.set('rhodecode', 'RC_SCM_DATA', json.dumps(extras))
119 config.set('rhodecode', 'RC_SCM_DATA', json.dumps(extras))
89 hg_config_org = config
120 hg_config_org = config
90
121
91 expected_config = [
122 expected_config = [
92 ('vcs_svn_tag', 'ff89f8c714d135d865f44b90e5413b88de19a55f', '/tags/*'),
123 ('vcs_svn_tag', 'ff89f8c714d135d865f44b90e5413b88de19a55f', '/tags/*'),
93 ('web', 'push_ssl', 'False'),
124 ('web', 'push_ssl', 'False'),
94 ('web', 'allow_push', '*'),
125 ('web', 'allow_push', '*'),
95 ('web', 'allow_archive', 'gz zip bz2'),
126 ('web', 'allow_archive', 'gz zip bz2'),
96 ('web', 'baseurl', '/'),
127 ('web', 'baseurl', '/'),
97 ('vcs_git_lfs', 'store_location', hg_config_org.get('vcs_git_lfs', 'store_location')),
128 ('vcs_git_lfs', 'store_location', hg_config_org.get('vcs_git_lfs', 'store_location')),
98 ('vcs_svn_branch', '9aac1a38c3b8a0cdc4ae0f960a5f83332bc4fa5e', '/branches/*'),
129 ('vcs_svn_branch', '9aac1a38c3b8a0cdc4ae0f960a5f83332bc4fa5e', '/branches/*'),
99 ('vcs_svn_branch', 'c7e6a611c87da06529fd0dd733308481d67c71a8', '/trunk'),
130 ('vcs_svn_branch', 'c7e6a611c87da06529fd0dd733308481d67c71a8', '/trunk'),
100 ('largefiles', 'usercache', hg_config_org.get('largefiles', 'usercache')),
131 ('largefiles', 'usercache', hg_config_org.get('largefiles', 'usercache')),
101 ('hooks', 'preoutgoing.pre_pull', 'python:vcsserver.hooks.pre_pull'),
132 ('hooks', 'preoutgoing.pre_pull', 'python:vcsserver.hooks.pre_pull'),
102 ('hooks', 'prechangegroup.pre_push', 'python:vcsserver.hooks.pre_push'),
133 ('hooks', 'prechangegroup.pre_push', 'python:vcsserver.hooks.pre_push'),
103 ('hooks', 'outgoing.pull_logger', 'python:vcsserver.hooks.log_pull_action'),
134 ('hooks', 'outgoing.pull_logger', 'python:vcsserver.hooks.log_pull_action'),
104 ('hooks', 'pretxnchangegroup.pre_push', 'python:vcsserver.hooks.pre_push'),
135 ('hooks', 'pretxnchangegroup.pre_push', 'python:vcsserver.hooks.pre_push'),
105 ('hooks', 'changegroup.push_logger', 'python:vcsserver.hooks.log_push_action'),
136 ('hooks', 'changegroup.push_logger', 'python:vcsserver.hooks.log_push_action'),
106 ('hooks', 'changegroup.repo_size', 'python:vcsserver.hooks.repo_size'),
137 ('hooks', 'changegroup.repo_size', 'python:vcsserver.hooks.repo_size'),
107 ('phases', 'publish', 'True'),
138 ('phases', 'publish', 'True'),
108 ('extensions', 'largefiles', ''),
139 ('extensions', 'largefiles', ''),
109 ('paths', '/', hg_config_org.get('paths', '/')),
140 ('paths', '/', hg_config_org.get('paths', '/')),
110 ('rhodecode', 'RC_SCM_DATA', '[["foo", "FOO", "bar", "BAR"]]')
141 ('rhodecode', 'RC_SCM_DATA', '[["foo", "FOO", "bar", "BAR"]]')
111 ]
142 ]
112 for entry in expected_config:
143 for entry in expected_config:
113 assert entry in hg_config
144 assert entry in hg_config
114
145
115
146
116 def test_create_wsgi_app_uses_scm_app_from_simplevcs(request_stub):
147 def test_create_wsgi_app_uses_scm_app_from_simplevcs(request_stub):
117 config = {
148 config = {
118 'auth_ret_code': '',
149 'auth_ret_code': '',
119 'base_path': '',
150 'base_path': '',
120 'vcs.scm_app_implementation':
151 'vcs.scm_app_implementation':
121 'rhodecode.tests.lib.middleware.mock_scm_app',
152 'rhodecode.tests.lib.middleware.mock_scm_app',
122 }
153 }
123 app = simplehg.SimpleHg(config=config, registry=request_stub.registry)
154 app = simplehg.SimpleHg(config=config, registry=request_stub.registry)
124 wsgi_app = app._create_wsgi_app('/tmp/test', 'test_repo', {})
155 wsgi_app = app._create_wsgi_app('/tmp/test', 'test_repo', {})
125 assert wsgi_app is mock_scm_app.mock_hg_wsgi
156 assert wsgi_app is mock_scm_app.mock_hg_wsgi
General Comments 0
You need to be logged in to leave comments. Login now