##// END OF EJS Templates
mercurial: protocol security updates....
marcink -
r2724:7a057a98 default
parent child Browse files
Show More
@@ -1,81 +1,152 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2010-2018 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 """
22 22 SimpleHG middleware for handling mercurial protocol request
23 23 (push/clone etc.). It's implemented with basic auth function
24 24 """
25 25
26 26 import logging
27 27 import urlparse
28 import urllib
28 29
29 30 from rhodecode.lib import utils
30 31 from rhodecode.lib.ext_json import json
31 32 from rhodecode.lib.middleware import simplevcs
32 33
33 34 log = logging.getLogger(__name__)
34 35
35 36
36 37 class SimpleHg(simplevcs.SimpleVCS):
37 38
38 39 SCM = 'hg'
39 40
40 41 def _get_repository_name(self, environ):
41 42 """
42 43 Gets repository name out of PATH_INFO header
43 44
44 45 :param environ: environ where PATH_INFO is stored
45 46 """
46 47 return environ['PATH_INFO'].strip('/')
47 48
48 49 _ACTION_MAPPING = {
49 50 'changegroup': 'pull',
50 51 'changegroupsubset': 'pull',
51 52 'getbundle': 'pull',
52 53 'stream_out': 'pull',
53 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 72 'unbundle': 'push',
55 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 121 def _get_action(self, environ):
59 122 """
60 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 126 :param environ:
64 127 """
128 default = 'push'
65 129 query = urlparse.parse_qs(environ['QUERY_STRING'],
66 130 keep_blank_values=True)
131
67 132 if 'cmd' in query:
68 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 144 def _create_wsgi_app(self, repo_path, repo_name, config):
74 145 return self.scm_app.create_hg_wsgi_app(
75 146 repo_path, repo_name, config)
76 147
77 148 def _create_config(self, extras, repo_name):
78 149 config = utils.make_db_config(repo=repo_name)
79 150 config.set('rhodecode', 'RC_SCM_DATA', json.dumps(extras))
80 151
81 152 return config.serialize()
@@ -1,125 +1,156 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2010-2018 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 import urlparse
22 22
23 23 import mock
24 24 import pytest
25 25 import simplejson as json
26 26
27 27 from rhodecode.lib.vcs.backends.base import Config
28 28 from rhodecode.tests.lib.middleware import mock_scm_app
29 29 import rhodecode.lib.middleware.simplehg as simplehg
30 30
31 31
32 32 def get_environ(url):
33 33 """Construct a minimum WSGI environ based on the URL."""
34 34 parsed_url = urlparse.urlparse(url)
35 35 environ = {
36 36 'PATH_INFO': parsed_url.path,
37 37 'QUERY_STRING': parsed_url.query,
38 38 }
39 39
40 40 return environ
41 41
42 42
43 43 @pytest.mark.parametrize(
44 44 'url, expected_action',
45 45 [
46 46 ('/foo/bar?cmd=unbundle&key=tip', 'push'),
47 47 ('/foo/bar?cmd=pushkey&key=tip', 'push'),
48 48 ('/foo/bar?cmd=listkeys&key=tip', 'pull'),
49 49 ('/foo/bar?cmd=changegroup&key=tip', 'pull'),
50 # Edge case: unknown argument: assume pull
51 ('/foo/bar?cmd=unknown&key=tip', 'pull'),
52 ('/foo/bar?cmd=&key=tip', 'pull'),
50 ('/foo/bar?cmd=hello', 'pull'),
51 ('/foo/bar?cmd=batch', 'push'),
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 56 # Edge case: not cmd argument
54 ('/foo/bar?key=tip', 'pull'),
57 ('/foo/bar?key=tip', 'push'),
55 58 ])
56 59 def test_get_action(url, expected_action, request_stub):
57 60 app = simplehg.SimpleHg(config={'auth_ret_code': '', 'base_path': ''},
58 61 registry=request_stub.registry)
59 62 assert expected_action == app._get_action(get_environ(url))
60 63
61 64
62 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 94 'url, expected_repo_name',
64 95 [
65 96 ('/foo?cmd=unbundle&key=tip', 'foo'),
66 97 ('/foo/bar?cmd=pushkey&key=tip', 'foo/bar'),
67 98 ('/foo/bar/baz?cmd=listkeys&key=tip', 'foo/bar/baz'),
68 99 # Repos with trailing slashes.
69 100 ('/foo/?cmd=unbundle&key=tip', 'foo'),
70 101 ('/foo/bar/?cmd=pushkey&key=tip', 'foo/bar'),
71 102 ('/foo/bar/baz/?cmd=listkeys&key=tip', 'foo/bar/baz'),
72 103 ])
73 104 def test_get_repository_name(url, expected_repo_name, request_stub):
74 105 app = simplehg.SimpleHg(config={'auth_ret_code': '', 'base_path': ''},
75 106 registry=request_stub.registry)
76 107 assert expected_repo_name == app._get_repository_name(get_environ(url))
77 108
78 109
79 110 def test_get_config(user_util, baseapp, request_stub):
80 111 repo = user_util.create_repo(repo_type='git')
81 112 app = simplehg.SimpleHg(config={'auth_ret_code': '', 'base_path': ''},
82 113 registry=request_stub.registry)
83 114 extras = [('foo', 'FOO', 'bar', 'BAR')]
84 115
85 116 hg_config = app._create_config(extras, repo_name=repo.repo_name)
86 117
87 118 config = simplehg.utils.make_db_config(repo=repo.repo_name)
88 119 config.set('rhodecode', 'RC_SCM_DATA', json.dumps(extras))
89 120 hg_config_org = config
90 121
91 122 expected_config = [
92 123 ('vcs_svn_tag', 'ff89f8c714d135d865f44b90e5413b88de19a55f', '/tags/*'),
93 124 ('web', 'push_ssl', 'False'),
94 125 ('web', 'allow_push', '*'),
95 126 ('web', 'allow_archive', 'gz zip bz2'),
96 127 ('web', 'baseurl', '/'),
97 128 ('vcs_git_lfs', 'store_location', hg_config_org.get('vcs_git_lfs', 'store_location')),
98 129 ('vcs_svn_branch', '9aac1a38c3b8a0cdc4ae0f960a5f83332bc4fa5e', '/branches/*'),
99 130 ('vcs_svn_branch', 'c7e6a611c87da06529fd0dd733308481d67c71a8', '/trunk'),
100 131 ('largefiles', 'usercache', hg_config_org.get('largefiles', 'usercache')),
101 132 ('hooks', 'preoutgoing.pre_pull', 'python:vcsserver.hooks.pre_pull'),
102 133 ('hooks', 'prechangegroup.pre_push', 'python:vcsserver.hooks.pre_push'),
103 134 ('hooks', 'outgoing.pull_logger', 'python:vcsserver.hooks.log_pull_action'),
104 135 ('hooks', 'pretxnchangegroup.pre_push', 'python:vcsserver.hooks.pre_push'),
105 136 ('hooks', 'changegroup.push_logger', 'python:vcsserver.hooks.log_push_action'),
106 137 ('hooks', 'changegroup.repo_size', 'python:vcsserver.hooks.repo_size'),
107 138 ('phases', 'publish', 'True'),
108 139 ('extensions', 'largefiles', ''),
109 140 ('paths', '/', hg_config_org.get('paths', '/')),
110 141 ('rhodecode', 'RC_SCM_DATA', '[["foo", "FOO", "bar", "BAR"]]')
111 142 ]
112 143 for entry in expected_config:
113 144 assert entry in hg_config
114 145
115 146
116 147 def test_create_wsgi_app_uses_scm_app_from_simplevcs(request_stub):
117 148 config = {
118 149 'auth_ret_code': '',
119 150 'base_path': '',
120 151 'vcs.scm_app_implementation':
121 152 'rhodecode.tests.lib.middleware.mock_scm_app',
122 153 }
123 154 app = simplehg.SimpleHg(config=config, registry=request_stub.registry)
124 155 wsgi_app = app._create_wsgi_app('/tmp/test', 'test_repo', {})
125 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