/brz/remove-bazaar

To get this branch, use:
bzr branch http://gegoxaren.bato24.eu/bzr/brz/remove-bazaar
6402.3.1 by Jelmer Vernooij
Add safe_open class to bzr.
1
# Copyright (C) 2011 Canonical Ltd
2
#
3
# This program is free software; you can redistribute it and/or modify
4
# it under the terms of the GNU General Public License as published by
5
# the Free Software Foundation; either version 2 of the License, or
6
# (at your option) any later version.
7
#
8
# This program is distributed in the hope that it will be useful,
9
# but WITHOUT ANY WARRANTY; without even the implied warranty of
10
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
11
# GNU General Public License for more details.
12
#
13
# You should have received a copy of the GNU General Public License
14
# along with this program; if not, write to the Free Software
15
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
16
17
"""Safe branch opening."""
18
19
import threading
20
21
from bzrlib import (
22
    errors,
23
    trace,
24
    urlutils,
25
    )
26
from bzrlib.branch import Branch
27
from bzrlib.controldir import ControlDirFormat
28
from bzrlib.transport import (
29
    do_catching_redirections,
30
    get_transport,
31
    )
32
33
6402.3.2 by Jelmer Vernooij
bzrify
34
class BadUrl(errors.BzrError):
35
36
    _fmt = "Tried to access a branch from bad URL %(url)s."
37
38
39
class BranchReferenceForbidden(errors.BzrError):
40
41
    _fmt = ("Trying to mirror a branch reference and the branch type "
42
            "does not allow references.")
43
44
45
class BranchLoopError(errors.BzrError):
6402.3.1 by Jelmer Vernooij
Add safe_open class to bzr.
46
    """Encountered a branch cycle.
47
48
    A URL may point to a branch reference or it may point to a stacked branch.
49
    In either case, it's possible for there to be a cycle in these references,
50
    and this exception is raised when we detect such a cycle.
51
    """
52
6402.3.2 by Jelmer Vernooij
bzrify
53
    _fmt = "Encountered a branch cycle"""
54
6402.3.1 by Jelmer Vernooij
Add safe_open class to bzr.
55
56
class BranchOpenPolicy(object):
57
    """Policy on how to open branches.
58
59
    In particular, a policy determines which branches are safe to open by
60
    checking their URLs and deciding whether or not to follow branch
61
    references.
62
    """
63
64
    def should_follow_references(self):
65
        """Whether we traverse references when mirroring.
66
67
        Subclasses must override this method.
68
69
        If we encounter a branch reference and this returns false, an error is
70
        raised.
71
72
        :returns: A boolean to indicate whether to follow a branch reference.
73
        """
74
        raise NotImplementedError(self.should_follow_references)
75
76
    def transform_fallback_location(self, branch, url):
77
        """Validate, maybe modify, 'url' to be used as a stacked-on location.
78
79
        :param branch:  The branch that is being opened.
80
        :param url: The URL that the branch provides for its stacked-on
81
            location.
82
        :return: (new_url, check) where 'new_url' is the URL of the branch to
83
            actually open and 'check' is true if 'new_url' needs to be
84
            validated by check_and_follow_branch_reference.
85
        """
86
        raise NotImplementedError(self.transform_fallback_location)
87
88
    def check_one_url(self, url):
89
        """Check the safety of the source URL.
90
91
        Subclasses must override this method.
92
93
        :param url: The source URL to check.
94
        :raise BadUrl: subclasses are expected to raise this or a subclass
95
            when it finds a URL it deems to be unsafe.
96
        """
97
        raise NotImplementedError(self.check_one_url)
98
99
100
class BlacklistPolicy(BranchOpenPolicy):
101
    """Branch policy that forbids certain URLs."""
102
103
    def __init__(self, should_follow_references, unsafe_urls=None):
104
        if unsafe_urls is None:
105
            unsafe_urls = set()
106
        self._unsafe_urls = unsafe_urls
107
        self._should_follow_references = should_follow_references
108
109
    def should_follow_references(self):
110
        return self._should_follow_references
111
112
    def check_one_url(self, url):
113
        if url in self._unsafe_urls:
114
            raise BadUrl(url)
115
116
    def transform_fallback_location(self, branch, url):
117
        """See `BranchOpenPolicy.transform_fallback_location`.
118
119
        This class is not used for testing our smarter stacking features so we
120
        just do the simplest thing: return the URL that would be used anyway
121
        and don't check it.
122
        """
123
        return urlutils.join(branch.base, url), False
124
125
126
class AcceptAnythingPolicy(BlacklistPolicy):
127
    """Accept anything, to make testing easier."""
128
129
    def __init__(self):
130
        super(AcceptAnythingPolicy, self).__init__(True, set())
131
132
133
class WhitelistPolicy(BranchOpenPolicy):
134
    """Branch policy that only allows certain URLs."""
135
136
    def __init__(self, should_follow_references, allowed_urls=None,
137
                 check=False):
138
        if allowed_urls is None:
139
            allowed_urls = []
140
        self.allowed_urls = set(url.rstrip('/') for url in allowed_urls)
141
        self.check = check
142
143
    def should_follow_references(self):
144
        return self._should_follow_references
145
146
    def check_one_url(self, url):
147
        if url.rstrip('/') not in self.allowed_urls:
148
            raise BadUrl(url)
149
150
    def transform_fallback_location(self, branch, url):
151
        """See `BranchOpenPolicy.transform_fallback_location`.
152
153
        Here we return the URL that would be used anyway and optionally check
154
        it.
155
        """
156
        return urlutils.join(branch.base, url), self.check
157
158
159
class SingleSchemePolicy(BranchOpenPolicy):
160
    """Branch open policy that rejects URLs not on the given scheme."""
161
162
    def __init__(self, allowed_scheme):
163
        self.allowed_scheme = allowed_scheme
164
165
    def should_follow_references(self):
166
        return True
167
168
    def transform_fallback_location(self, branch, url):
169
        return urlutils.join(branch.base, url), True
170
171
    def check_one_url(self, url):
172
        """Check that `url` is safe to open."""
173
        if urlutils.URL.from_string(str(url)).scheme != self.allowed_scheme:
174
            raise BadUrl(url)
175
176
177
class SafeBranchOpener(object):
178
    """Safe branch opener.
179
180
    All locations that are opened (stacked-on branches, references) are
181
    checked against a policy object.
182
183
    The policy object is expected to have the following methods:
184
    * check_one_url 
185
    * should_follow_references
186
    * transform_fallback_location
187
    """
188
189
    _threading_data = threading.local()
190
191
    def __init__(self, policy, probers=None):
192
        """Create a new SafeBranchOpener.
193
194
        :param policy: The opener policy to use.
195
        :param probers: Optional list of probers to allow.
196
            Defaults to local and remote bzr probers.
197
        """
198
        self.policy = policy
199
        self._seen_urls = set()
200
        if probers is None:
201
            self.probers = ControlDirFormat.all_probers()
202
        else:
203
            self.probers = probers
204
205
    @classmethod
206
    def install_hook(cls):
207
        """Install the ``transform_fallback_location`` hook.
208
209
        This is done at module import time, but transform_fallback_locationHook
210
        doesn't do anything unless the `_active_openers` threading.Local
211
        object has a 'opener' attribute in this thread.
212
213
        This is in a module-level function rather than performed at module
214
        level so that it can be called in setUp for testing `SafeBranchOpener`
215
        as bzrlib.tests.TestCase.setUp clears hooks.
216
        """
217
        Branch.hooks.install_named_hook(
218
            'transform_fallback_location',
219
            cls.transform_fallback_locationHook,
220
            'SafeBranchOpener.transform_fallback_locationHook')
221
222
    def check_and_follow_branch_reference(self, url):
223
        """Check URL (and possibly the referenced URL) for safety.
224
225
        This method checks that `url` passes the policy's `check_one_url`
226
        method, and if `url` refers to a branch reference, it checks whether
227
        references are allowed and whether the reference's URL passes muster
228
        also -- recursively, until a real branch is found.
229
230
        :param url: URL to check
231
        :raise BranchLoopError: If the branch references form a loop.
232
        :raise BranchReferenceForbidden: If this opener forbids branch
233
            references.
234
        """
235
        while True:
236
            if url in self._seen_urls:
237
                raise BranchLoopError()
238
            self._seen_urls.add(url)
239
            self.policy.check_one_url(url)
240
            next_url = self.follow_reference(url)
241
            if next_url is None:
242
                return url
243
            url = next_url
244
            if not self.policy.should_follow_references():
245
                raise BranchReferenceForbidden(url)
246
247
    @classmethod
248
    def transform_fallback_locationHook(cls, branch, url):
249
        """Installed as the 'transform_fallback_location' Branch hook.
250
251
        This method calls `transform_fallback_location` on the policy object and
252
        either returns the url it provides or passes it back to
253
        check_and_follow_branch_reference.
254
        """
255
        try:
256
            opener = getattr(cls._threading_data, "opener")
257
        except AttributeError:
258
            return url
259
        new_url, check = opener.policy.transform_fallback_location(branch, url)
260
        if check:
261
            return opener.check_and_follow_branch_reference(new_url)
262
        else:
263
            return new_url
264
265
    def run_with_transform_fallback_location_hook_installed(
266
            self, callable, *args, **kw):
267
        assert (self.transform_fallback_locationHook in
268
                Branch.hooks['transform_fallback_location'])
269
        self._threading_data.opener = self
270
        try:
271
            return callable(*args, **kw)
272
        finally:
273
            del self._threading_data.opener
274
            # We reset _seen_urls here to avoid multiple calls to open giving
275
            # spurious loop exceptions.
276
            self._seen_urls = set()
277
278
    def follow_reference(self, url):
279
        """Get the branch-reference value at the specified url.
280
281
        This exists as a separate method only to be overriden in unit tests.
282
        """
283
        bzrdir = self._open_dir(url)
284
        return bzrdir.get_branch_reference()
285
286
    def _open_dir(self, url):
287
        """Simple BzrDir.open clone that only uses specific probers.
288
289
        :param url: URL to open
290
        :return: ControlDir instance
291
        """
292
        def redirected(transport, e, redirection_notice):
293
            self.policy.check_one_url(e.target)
294
            redirected_transport = transport._redirected_to(
295
                e.source, e.target)
296
            if redirected_transport is None:
297
                raise errors.NotBranchError(e.source)
298
            trace.note('%s is%s redirected to %s',
299
                 transport.base, e.permanently, redirected_transport.base)
300
            return redirected_transport
301
302
        def find_format(transport):
303
            last_error = errors.NotBranchError(transport.base)
304
            for prober_kls in self.probers:
305
                prober = prober_kls()
306
                try:
307
                    return transport, prober.probe_transport(transport)
308
                except errors.NotBranchError, e:
309
                    last_error = e
310
            else:
311
                raise last_error
312
        transport = get_transport(url)
313
        transport, format = do_catching_redirections(find_format, transport,
314
            redirected)
315
        return format.open(transport)
316
317
    def open(self, url):
318
        """Open the Bazaar branch at url, first checking for safety.
319
320
        What safety means is defined by a subclasses `follow_reference` and
321
        `check_one_url` methods.
322
        """
6402.3.2 by Jelmer Vernooij
bzrify
323
        if type(url) != str:
324
            raise TypeError
325
6402.3.1 by Jelmer Vernooij
Add safe_open class to bzr.
326
        url = self.check_and_follow_branch_reference(url)
327
328
        def open_branch(url):
329
            dir = self._open_dir(url)
330
            return dir.open_branch()
331
        return self.run_with_transform_fallback_location_hook_installed(
332
            open_branch, url)
333
334
335
def safe_open(allowed_scheme, url):
336
    """Open the branch at `url`, only accessing URLs on `allowed_scheme`.
337
338
    :raises BadUrl: An attempt was made to open a URL that was not on
339
        `allowed_scheme`.
340
    """
341
    return SafeBranchOpener(SingleSchemePolicy(allowed_scheme)).open(url)
342
343
344
SafeBranchOpener.install_hook()