~launchpad-pqm/launchpad/devel

« back to all changes in this revision

Viewing changes to lib/lp/codehosting/safe_open.py

  • Committer: Launchpad Patch Queue Manager
  • Date: 2011-08-18 22:05:32 UTC
  • mfrom: (13677.5.8 safe-opener-threading)
  • Revision ID: launchpad@pqm.canonical.com-20110818220532-zxyairkgkd3v2fa8
[r=gmb][bug=826082,826136] Make SafeBranchOpener threadsafe.

Show diffs side-by-side

added added

removed removed

Lines of Context:
11
11
 
12
12
from lazr.uri import URI
13
13
 
 
14
import threading
 
15
 
14
16
__all__ = [
15
17
    'AcceptAnythingPolicy',
16
18
    'BadUrl',
150
152
        return urlutils.join(branch.base, url), self.check
151
153
 
152
154
 
 
155
class SingleSchemePolicy(BranchOpenPolicy):
 
156
    """Branch open policy that rejects URLs not on the given scheme."""
 
157
 
 
158
    def __init__(self, allowed_scheme):
 
159
        self.allowed_scheme = allowed_scheme
 
160
 
 
161
    def shouldFollowReferences(self):
 
162
        return True
 
163
 
 
164
    def transformFallbackLocation(self, branch, url):
 
165
        return urlutils.join(branch.base, url), True
 
166
 
 
167
    def checkOneURL(self, url):
 
168
        """Check that `url` is safe to open."""
 
169
        if URI(url).scheme != self.allowed_scheme:
 
170
            raise BadUrl(url)
 
171
 
 
172
 
153
173
class SafeBranchOpener(object):
154
174
    """Safe branch opener.
155
175
 
 
176
    All locations that are opened (stacked-on branches, references) are checked
 
177
    against a policy object.
 
178
 
156
179
    The policy object is expected to have the following methods:
157
180
    * checkOneURL
158
181
    * shouldFollowReferences
159
182
    * transformFallbackLocation
160
183
    """
161
184
 
 
185
    _threading_data = threading.local()
 
186
 
162
187
    def __init__(self, policy):
163
188
        self.policy = policy
164
189
        self._seen_urls = set()
165
190
 
 
191
    @classmethod
 
192
    def install_hook(cls):
 
193
        """Install the ``transformFallbackLocation`` hook.
 
194
 
 
195
        This is done at module import time, but transformFallbackLocationHook
 
196
        doesn't do anything unless the `_active_openers` threading.Local object
 
197
        has a 'opener' attribute in this thread.
 
198
 
 
199
        This is in a module-level function rather than performed at module level
 
200
        so that it can be called in setUp for testing `SafeBranchOpener` as
 
201
        bzrlib.tests.TestCase.setUp clears hooks.
 
202
        """
 
203
        Branch.hooks.install_named_hook(
 
204
            'transform_fallback_location',
 
205
            cls.transformFallbackLocationHook,
 
206
            'SafeBranchOpener.transformFallbackLocationHook')
 
207
 
166
208
    def checkAndFollowBranchReference(self, url):
167
209
        """Check URL (and possibly the referenced URL) for safety.
168
210
 
187
229
            if not self.policy.shouldFollowReferences():
188
230
                raise BranchReferenceForbidden(url)
189
231
 
190
 
    def transformFallbackLocationHook(self, branch, url):
 
232
    @classmethod
 
233
    def transformFallbackLocationHook(cls, branch, url):
191
234
        """Installed as the 'transform_fallback_location' Branch hook.
192
235
 
193
236
        This method calls `transformFallbackLocation` on the policy object and
194
237
        either returns the url it provides or passes it back to
195
238
        checkAndFollowBranchReference.
196
239
        """
197
 
        new_url, check = self.policy.transformFallbackLocation(branch, url)
 
240
        try:
 
241
            opener = getattr(cls._threading_data, "opener")
 
242
        except AttributeError:
 
243
            return url
 
244
        new_url, check = opener.policy.transformFallbackLocation(branch, url)
198
245
        if check:
199
 
            return self.checkAndFollowBranchReference(new_url)
 
246
            return opener.checkAndFollowBranchReference(new_url)
200
247
        else:
201
248
            return new_url
202
249
 
203
250
    def runWithTransformFallbackLocationHookInstalled(
204
251
            self, callable, *args, **kw):
205
 
        Branch.hooks.install_named_hook(
206
 
            'transform_fallback_location', self.transformFallbackLocationHook,
207
 
            'SafeBranchOpener.transformFallbackLocationHook')
 
252
        assert (self.transformFallbackLocationHook in
 
253
                Branch.hooks['transform_fallback_location'])
 
254
        self._threading_data.opener = self
208
255
        try:
209
256
            return callable(*args, **kw)
210
257
        finally:
211
 
            # XXX 2008-11-24 MichaelHudson, bug=301472: This is the hacky way
212
 
            # to remove a hook.  The linked bug report asks for an API to do
213
 
            # it.
214
 
            Branch.hooks['transform_fallback_location'].remove(
215
 
                self.transformFallbackLocationHook)
 
258
            del self._threading_data.opener
216
259
            # We reset _seen_urls here to avoid multiple calls to open giving
217
260
            # spurious loop exceptions.
218
261
            self._seen_urls = set()
236
279
            Branch.open, url)
237
280
 
238
281
 
239
 
class URLChecker(BranchOpenPolicy):
240
 
    """Branch open policy that rejects URLs not on the given scheme."""
241
 
 
242
 
    def __init__(self, allowed_scheme):
243
 
        self.allowed_scheme = allowed_scheme
244
 
 
245
 
    def shouldFollowReferences(self):
246
 
        return True
247
 
 
248
 
    def transformFallbackLocation(self, branch, url):
249
 
        return urlutils.join(branch.base, url), True
250
 
 
251
 
    def checkOneURL(self, url):
252
 
        """Check that `url` is safe to open."""
253
 
        if URI(url).scheme != self.allowed_scheme:
254
 
            raise BadUrl(url)
255
 
 
256
 
 
257
282
def safe_open(allowed_scheme, url):
258
283
    """Open the branch at `url`, only accessing URLs on `allowed_scheme`.
259
284
 
260
285
    :raises BadUrl: An attempt was made to open a URL that was not on
261
286
        `allowed_scheme`.
262
287
    """
263
 
    return SafeBranchOpener(URLChecker(allowed_scheme)).open(url)
 
288
    return SafeBranchOpener(SingleSchemePolicy(allowed_scheme)).open(url)
 
289
 
 
290
 
 
291
SafeBranchOpener.install_hook()