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 |
urlutils, |
|
24 |
)
|
|
25 |
from bzrlib.branch import Branch |
|
|
6402.3.3
by Jelmer Vernooij
Simplify safe open a bit more. |
26 |
from bzrlib.controldir import ( |
27 |
ControlDir, |
|
|
6402.3.1
by Jelmer Vernooij
Add safe_open class to bzr. |
28 |
)
|
29 |
||
30 |
||
|
6402.3.2
by Jelmer Vernooij
bzrify |
31 |
class BadUrl(errors.BzrError): |
32 |
||
33 |
_fmt = "Tried to access a branch from bad URL %(url)s." |
|
34 |
||
35 |
||
36 |
class BranchReferenceForbidden(errors.BzrError): |
|
37 |
||
38 |
_fmt = ("Trying to mirror a branch reference and the branch type " |
|
39 |
"does not allow references.") |
|
40 |
||
41 |
||
42 |
class BranchLoopError(errors.BzrError): |
|
|
6402.3.1
by Jelmer Vernooij
Add safe_open class to bzr. |
43 |
"""Encountered a branch cycle. |
44 |
||
45 |
A URL may point to a branch reference or it may point to a stacked branch.
|
|
46 |
In either case, it's possible for there to be a cycle in these references,
|
|
47 |
and this exception is raised when we detect such a cycle.
|
|
48 |
"""
|
|
49 |
||
|
6402.3.2
by Jelmer Vernooij
bzrify |
50 |
_fmt = "Encountered a branch cycle""" |
51 |
||
|
6402.3.1
by Jelmer Vernooij
Add safe_open class to bzr. |
52 |
|
53 |
class BranchOpenPolicy(object): |
|
54 |
"""Policy on how to open branches. |
|
55 |
||
56 |
In particular, a policy determines which branches are safe to open by
|
|
57 |
checking their URLs and deciding whether or not to follow branch
|
|
58 |
references.
|
|
59 |
"""
|
|
60 |
||
61 |
def should_follow_references(self): |
|
62 |
"""Whether we traverse references when mirroring. |
|
63 |
||
64 |
Subclasses must override this method.
|
|
65 |
||
66 |
If we encounter a branch reference and this returns false, an error is
|
|
67 |
raised.
|
|
68 |
||
69 |
:returns: A boolean to indicate whether to follow a branch reference.
|
|
70 |
"""
|
|
71 |
raise NotImplementedError(self.should_follow_references) |
|
72 |
||
73 |
def transform_fallback_location(self, branch, url): |
|
74 |
"""Validate, maybe modify, 'url' to be used as a stacked-on location. |
|
75 |
||
76 |
:param branch: The branch that is being opened.
|
|
77 |
:param url: The URL that the branch provides for its stacked-on
|
|
78 |
location.
|
|
79 |
:return: (new_url, check) where 'new_url' is the URL of the branch to
|
|
80 |
actually open and 'check' is true if 'new_url' needs to be
|
|
81 |
validated by check_and_follow_branch_reference.
|
|
82 |
"""
|
|
83 |
raise NotImplementedError(self.transform_fallback_location) |
|
84 |
||
85 |
def check_one_url(self, url): |
|
86 |
"""Check the safety of the source URL. |
|
87 |
||
88 |
Subclasses must override this method.
|
|
89 |
||
90 |
:param url: The source URL to check.
|
|
91 |
:raise BadUrl: subclasses are expected to raise this or a subclass
|
|
92 |
when it finds a URL it deems to be unsafe.
|
|
93 |
"""
|
|
94 |
raise NotImplementedError(self.check_one_url) |
|
95 |
||
96 |
||
97 |
class BlacklistPolicy(BranchOpenPolicy): |
|
98 |
"""Branch policy that forbids certain URLs.""" |
|
99 |
||
100 |
def __init__(self, should_follow_references, unsafe_urls=None): |
|
101 |
if unsafe_urls is None: |
|
102 |
unsafe_urls = set() |
|
103 |
self._unsafe_urls = unsafe_urls |
|
104 |
self._should_follow_references = should_follow_references |
|
105 |
||
106 |
def should_follow_references(self): |
|
107 |
return self._should_follow_references |
|
108 |
||
109 |
def check_one_url(self, url): |
|
110 |
if url in self._unsafe_urls: |
|
111 |
raise BadUrl(url) |
|
112 |
||
113 |
def transform_fallback_location(self, branch, url): |
|
114 |
"""See `BranchOpenPolicy.transform_fallback_location`. |
|
115 |
||
116 |
This class is not used for testing our smarter stacking features so we
|
|
117 |
just do the simplest thing: return the URL that would be used anyway
|
|
118 |
and don't check it.
|
|
119 |
"""
|
|
120 |
return urlutils.join(branch.base, url), False |
|
121 |
||
122 |
||
123 |
class AcceptAnythingPolicy(BlacklistPolicy): |
|
124 |
"""Accept anything, to make testing easier.""" |
|
125 |
||
126 |
def __init__(self): |
|
127 |
super(AcceptAnythingPolicy, self).__init__(True, set()) |
|
128 |
||
129 |
||
130 |
class WhitelistPolicy(BranchOpenPolicy): |
|
131 |
"""Branch policy that only allows certain URLs.""" |
|
132 |
||
133 |
def __init__(self, should_follow_references, allowed_urls=None, |
|
134 |
check=False): |
|
135 |
if allowed_urls is None: |
|
136 |
allowed_urls = [] |
|
137 |
self.allowed_urls = set(url.rstrip('/') for url in allowed_urls) |
|
138 |
self.check = check |
|
139 |
||
140 |
def should_follow_references(self): |
|
141 |
return self._should_follow_references |
|
142 |
||
143 |
def check_one_url(self, url): |
|
144 |
if url.rstrip('/') not in self.allowed_urls: |
|
145 |
raise BadUrl(url) |
|
146 |
||
147 |
def transform_fallback_location(self, branch, url): |
|
148 |
"""See `BranchOpenPolicy.transform_fallback_location`. |
|
149 |
||
150 |
Here we return the URL that would be used anyway and optionally check
|
|
151 |
it.
|
|
152 |
"""
|
|
153 |
return urlutils.join(branch.base, url), self.check |
|
154 |
||
155 |
||
156 |
class SingleSchemePolicy(BranchOpenPolicy): |
|
157 |
"""Branch open policy that rejects URLs not on the given scheme.""" |
|
158 |
||
159 |
def __init__(self, allowed_scheme): |
|
160 |
self.allowed_scheme = allowed_scheme |
|
161 |
||
162 |
def should_follow_references(self): |
|
163 |
return True |
|
164 |
||
165 |
def transform_fallback_location(self, branch, url): |
|
166 |
return urlutils.join(branch.base, url), True |
|
167 |
||
168 |
def check_one_url(self, url): |
|
169 |
"""Check that `url` is safe to open.""" |
|
170 |
if urlutils.URL.from_string(str(url)).scheme != self.allowed_scheme: |
|
171 |
raise BadUrl(url) |
|
172 |
||
173 |
||
174 |
class SafeBranchOpener(object): |
|
175 |
"""Safe branch opener. |
|
176 |
||
177 |
All locations that are opened (stacked-on branches, references) are
|
|
178 |
checked against a policy object.
|
|
179 |
||
180 |
The policy object is expected to have the following methods:
|
|
181 |
* check_one_url
|
|
182 |
* should_follow_references
|
|
183 |
* transform_fallback_location
|
|
184 |
"""
|
|
185 |
||
186 |
_threading_data = threading.local() |
|
187 |
||
188 |
def __init__(self, policy, probers=None): |
|
189 |
"""Create a new SafeBranchOpener. |
|
190 |
||
191 |
:param policy: The opener policy to use.
|
|
192 |
:param probers: Optional list of probers to allow.
|
|
193 |
Defaults to local and remote bzr probers.
|
|
194 |
"""
|
|
195 |
self.policy = policy |
|
196 |
self._seen_urls = set() |
|
|
6402.3.3
by Jelmer Vernooij
Simplify safe open a bit more. |
197 |
self.probers = probers |
|
6402.3.1
by Jelmer Vernooij
Add safe_open class to bzr. |
198 |
|
199 |
@classmethod
|
|
200 |
def install_hook(cls): |
|
201 |
"""Install the ``transform_fallback_location`` hook. |
|
202 |
||
203 |
This is done at module import time, but transform_fallback_locationHook
|
|
204 |
doesn't do anything unless the `_active_openers` threading.Local
|
|
205 |
object has a 'opener' attribute in this thread.
|
|
206 |
||
207 |
This is in a module-level function rather than performed at module
|
|
208 |
level so that it can be called in setUp for testing `SafeBranchOpener`
|
|
209 |
as bzrlib.tests.TestCase.setUp clears hooks.
|
|
210 |
"""
|
|
211 |
Branch.hooks.install_named_hook( |
|
212 |
'transform_fallback_location', |
|
213 |
cls.transform_fallback_locationHook, |
|
214 |
'SafeBranchOpener.transform_fallback_locationHook') |
|
215 |
||
216 |
def check_and_follow_branch_reference(self, url): |
|
217 |
"""Check URL (and possibly the referenced URL) for safety. |
|
218 |
||
219 |
This method checks that `url` passes the policy's `check_one_url`
|
|
220 |
method, and if `url` refers to a branch reference, it checks whether
|
|
221 |
references are allowed and whether the reference's URL passes muster
|
|
222 |
also -- recursively, until a real branch is found.
|
|
223 |
||
224 |
:param url: URL to check
|
|
225 |
:raise BranchLoopError: If the branch references form a loop.
|
|
226 |
:raise BranchReferenceForbidden: If this opener forbids branch
|
|
227 |
references.
|
|
228 |
"""
|
|
229 |
while True: |
|
230 |
if url in self._seen_urls: |
|
231 |
raise BranchLoopError() |
|
232 |
self._seen_urls.add(url) |
|
233 |
self.policy.check_one_url(url) |
|
234 |
next_url = self.follow_reference(url) |
|
235 |
if next_url is None: |
|
236 |
return url |
|
237 |
url = next_url |
|
238 |
if not self.policy.should_follow_references(): |
|
239 |
raise BranchReferenceForbidden(url) |
|
240 |
||
241 |
@classmethod
|
|
242 |
def transform_fallback_locationHook(cls, branch, url): |
|
243 |
"""Installed as the 'transform_fallback_location' Branch hook. |
|
244 |
||
245 |
This method calls `transform_fallback_location` on the policy object and
|
|
246 |
either returns the url it provides or passes it back to
|
|
247 |
check_and_follow_branch_reference.
|
|
248 |
"""
|
|
249 |
try: |
|
250 |
opener = getattr(cls._threading_data, "opener") |
|
251 |
except AttributeError: |
|
252 |
return url |
|
253 |
new_url, check = opener.policy.transform_fallback_location(branch, url) |
|
254 |
if check: |
|
255 |
return opener.check_and_follow_branch_reference(new_url) |
|
256 |
else: |
|
257 |
return new_url |
|
258 |
||
259 |
def run_with_transform_fallback_location_hook_installed( |
|
260 |
self, callable, *args, **kw): |
|
261 |
assert (self.transform_fallback_locationHook in |
|
262 |
Branch.hooks['transform_fallback_location']) |
|
263 |
self._threading_data.opener = self |
|
264 |
try: |
|
265 |
return callable(*args, **kw) |
|
266 |
finally: |
|
267 |
del self._threading_data.opener |
|
268 |
# We reset _seen_urls here to avoid multiple calls to open giving
|
|
269 |
# spurious loop exceptions.
|
|
270 |
self._seen_urls = set() |
|
271 |
||
272 |
def follow_reference(self, url): |
|
273 |
"""Get the branch-reference value at the specified url. |
|
274 |
||
275 |
This exists as a separate method only to be overriden in unit tests.
|
|
276 |
"""
|
|
|
6402.3.3
by Jelmer Vernooij
Simplify safe open a bit more. |
277 |
bzrdir = ControlDir.open(url, probers=self.probers) |
|
6402.3.1
by Jelmer Vernooij
Add safe_open class to bzr. |
278 |
return bzrdir.get_branch_reference() |
279 |
||
280 |
def open(self, url): |
|
281 |
"""Open the Bazaar branch at url, first checking for safety. |
|
282 |
||
283 |
What safety means is defined by a subclasses `follow_reference` and
|
|
284 |
`check_one_url` methods.
|
|
285 |
"""
|
|
|
6402.3.2
by Jelmer Vernooij
bzrify |
286 |
if type(url) != str: |
287 |
raise TypeError |
|
288 |
||
|
6402.3.1
by Jelmer Vernooij
Add safe_open class to bzr. |
289 |
url = self.check_and_follow_branch_reference(url) |
290 |
||
291 |
def open_branch(url): |
|
|
6402.3.3
by Jelmer Vernooij
Simplify safe open a bit more. |
292 |
dir = ControlDir.open(url, probers=self.probers) |
|
6402.3.1
by Jelmer Vernooij
Add safe_open class to bzr. |
293 |
return dir.open_branch() |
294 |
return self.run_with_transform_fallback_location_hook_installed( |
|
295 |
open_branch, url) |
|
296 |
||
297 |
||
298 |
def safe_open(allowed_scheme, url): |
|
299 |
"""Open the branch at `url`, only accessing URLs on `allowed_scheme`. |
|
300 |
||
301 |
:raises BadUrl: An attempt was made to open a URL that was not on
|
|
302 |
`allowed_scheme`.
|
|
303 |
"""
|
|
304 |
return SafeBranchOpener(SingleSchemePolicy(allowed_scheme)).open(url) |
|
305 |
||
306 |
||
307 |
SafeBranchOpener.install_hook() |