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() |