source: trunk/src/allmydata/testing/web.py

Last change on this file was 7ab0483, checked in by Itamar Turner-Trauring <itamar@…>, at 2023-12-11T15:09:50Z

Pacify newer mypy

  • Property mode set to 100644
File size: 8.8 KB
Line 
1# -*- coding: utf-8 -*-
2# Tahoe-LAFS -- secure, distributed storage grid
3#
4# Copyright © 2020 The Tahoe-LAFS Software Foundation
5#
6# This file is part of Tahoe-LAFS.
7#
8# See the docs/about.rst file for licensing information.
9
10"""
11Test-helpers for clients that use the WebUI.
12"""
13
14from __future__ import annotations
15
16import hashlib
17from typing import Iterable
18
19import attr
20
21from hyperlink import DecodedURL
22
23from twisted.web.resource import (
24    Resource,
25)
26from twisted.web.iweb import (
27    IBodyProducer,
28)
29from twisted.web import (
30    http,
31)
32
33from twisted.internet.defer import (
34    succeed,
35)
36
37from treq.client import (
38    HTTPClient,
39    FileBodyProducer,
40)
41from treq.testing import (
42    RequestTraversalAgent,
43)
44from zope.interface import implementer
45
46import allmydata.uri
47from allmydata.util import (
48    base32,
49)
50from ..util.dictutil import BytesKeyDict
51
52
53__all__ = (
54    "create_fake_tahoe_root",
55    "create_tahoe_treq_client",
56)
57
58
59class _FakeTahoeRoot(Resource, object):
60    """
61    An in-memory 'fake' of a Tahoe WebUI root. Currently it only
62    implements (some of) the `/uri` resource.
63    """
64
65    def __init__(self, uri=None):
66        """
67        :param uri: a Resource to handle the `/uri` tree.
68        """
69        Resource.__init__(self)  # this is an old-style class :(
70        self._uri = uri
71        self.putChild(b"uri", self._uri)
72
73    def add_data(self, kind, data):
74        fresh, cap = self._uri.add_data(kind, data)
75        return cap
76
77
78KNOWN_CAPABILITIES = [
79    getattr(allmydata.uri, t).BASE_STRING
80    for t in dir(allmydata.uri)
81    if hasattr(getattr(allmydata.uri, t), 'BASE_STRING')
82]
83
84
85def capability_generator(kind):
86    """
87    Deterministically generates a stream of valid capabilities of the
88    given kind. The N, K and size values aren't related to anything
89    real.
90
91    :param bytes kind: the kind of capability, like `URI:CHK`
92
93    :returns: a generator that yields new capablities of a particular
94        kind.
95    """
96    if not isinstance(kind, bytes):
97        raise TypeError("'kind' must be bytes")
98
99    if kind not in KNOWN_CAPABILITIES:
100        raise ValueError(
101            "Unknown capability kind '{}' (valid are {})".format(
102                kind.decode('ascii'),
103                ", ".join([x.decode('ascii') for x in KNOWN_CAPABILITIES]),
104            )
105        )
106    # what we do here is to start with empty hashers for the key and
107    # ueb_hash and repeatedly feed() them a zero byte on each
108    # iteration .. so the same sequence of capabilities will always be
109    # produced. We could add a seed= argument if we wanted to produce
110    # different sequences.
111    number = 0
112    key_hasher = hashlib.new("sha256")
113    ueb_hasher = hashlib.new("sha256")  # ueb means "URI Extension Block"
114
115    # capabilities are "prefix:<128-bits-base32>:<256-bits-base32>:N:K:size"
116    while True:
117        number += 1
118        key_hasher.update(b"\x00")
119        ueb_hasher.update(b"\x00")
120
121        key = base32.b2a(key_hasher.digest()[:16])  # key is 16 bytes
122        ueb_hash = base32.b2a(ueb_hasher.digest())  # ueb hash is 32 bytes
123
124        cap = u"{kind}{key}:{ueb_hash}:{n}:{k}:{size}".format(
125            kind=kind.decode('ascii'),
126            key=key.decode('ascii'),
127            ueb_hash=ueb_hash.decode('ascii'),
128            n=1,
129            k=1,
130            size=number * 1000,
131        )
132        yield cap.encode("ascii")
133
134
135@attr.s
136class _FakeTahoeUriHandler(Resource, object):
137    """
138    An in-memory fake of (some of) the `/uri` endpoint of a Tahoe
139    WebUI
140    """
141
142    isLeaf = True
143
144    data: BytesKeyDict = attr.ib(default=attr.Factory(BytesKeyDict))
145    capability_generators: dict[bytes,Iterable[bytes]] = attr.ib(default=attr.Factory(dict))
146
147    def _generate_capability(self, kind):
148        """
149        :param str kind: any valid capability-string type
150
151        :returns: the next capability-string for the given kind
152        """
153        if kind not in self.capability_generators:
154            self.capability_generators[kind] = capability_generator(kind)
155        capability = next(self.capability_generators[kind])
156        return capability
157
158    def add_data(self, kind, data):
159        """
160        adds some data to our grid
161
162        :returns: a two-tuple: a bool (True if the data is freshly added) and a capability-string
163        """
164        if not isinstance(kind, bytes):
165            raise TypeError("'kind' must be bytes")
166        if not isinstance(data, bytes):
167            raise TypeError("'data' must be bytes")
168
169        for k in self.data:
170            if self.data[k] == data:
171                return (False, k)
172
173        cap = self._generate_capability(kind)
174        # it should be impossible for this to already be in our data,
175        # but check anyway to be sure
176        if cap in self.data:
177            raise Exception("Internal error; key already exists somehow")
178        self.data[cap] = data
179        return (True, cap)
180
181    def render_PUT(self, request):
182        data = request.content.read()
183        fresh, cap = self.add_data(b"URI:CHK:", data)
184        if fresh:
185            request.setResponseCode(http.CREATED)  # real code does this for brand-new files
186        else:
187            request.setResponseCode(http.OK)  # replaced/modified files
188        return cap
189
190    def render_POST(self, request):
191        t = request.args[u"t"][0]
192        data = request.content.read()
193
194        type_to_kind = {
195            "mkdir-immutable": b"URI:DIR2-CHK:"
196        }
197        kind = type_to_kind[t]
198        fresh, cap = self.add_data(kind, data)
199        return cap
200
201    def render_GET(self, request):
202        uri = DecodedURL.from_text(request.uri.decode('utf8'))
203        capability = None
204        for arg, value in uri.query:
205            if arg == u"uri":
206                capability = value.encode("utf-8")
207        # it's legal to use the form "/uri/<capability>"
208        if capability is None and request.postpath and request.postpath[0]:
209            capability = request.postpath[0]
210
211        # if we don't yet have a capability, that's an error
212        if capability is None:
213            request.setResponseCode(http.BAD_REQUEST)
214            return b"GET /uri requires uri="
215
216        # the user gave us a capability; if our Grid doesn't have any
217        # data for it, that's an error.
218        if capability not in self.data:
219            request.setResponseCode(http.GONE)
220            return u"No data for '{}'".format(capability.decode('ascii')).encode("utf-8")
221
222        return self.data[capability]
223
224
225def create_fake_tahoe_root():
226    """
227    If you wish to pre-populate data into the fake Tahoe grid, retain
228    a reference to this root by creating it yourself and passing it to
229    `create_tahoe_treq_client`. For example::
230
231        root = create_fake_tahoe_root()
232        cap_string = root.add_data(...)
233        client = create_tahoe_treq_client(root)
234
235    :returns: an IResource instance that will handle certain Tahoe URI
236        endpoints similar to a real Tahoe server.
237    """
238    root = _FakeTahoeRoot(
239        uri=_FakeTahoeUriHandler(),
240    )
241    return root
242
243
244@implementer(IBodyProducer)
245class _SynchronousProducer(object):
246    """
247    A partial implementation of an :obj:`IBodyProducer` which produces its
248    entire payload immediately.  There is no way to access to an instance of
249    this object from :obj:`RequestTraversalAgent` or :obj:`StubTreq`, or even a
250    :obj:`Resource: passed to :obj:`StubTreq`.
251
252    This does not implement the :func:`IBodyProducer.stopProducing` method,
253    because that is very difficult to trigger.  (The request from
254    `RequestTraversalAgent` would have to be canceled while it is still in the
255    transmitting state), and the intent is to use `RequestTraversalAgent` to
256    make synchronous requests.
257    """
258
259    def __init__(self, body):
260        """
261        Create a synchronous producer with some bytes.
262        """
263        if isinstance(body, FileBodyProducer):
264            body = body._inputFile.read()
265
266        if not isinstance(body, bytes):
267            raise ValueError(
268                "'body' must be bytes not '{}'".format(type(body))
269            )
270        self.body = body
271        self.length = len(body)
272
273    def startProducing(self, consumer):
274        """
275        Immediately produce all data.
276        """
277        consumer.write(self.body)
278        return succeed(None)
279
280    def stopProducing(self):
281        pass
282
283    def pauseProducing(self):
284        pass
285
286    def resumeProducing(self):
287        pass
288
289
290def create_tahoe_treq_client(root=None):
291    """
292    :param root: an instance created via `create_fake_tahoe_root`. The
293        caller might want a copy of this to call `.add_data` for example.
294
295    :returns: an instance of treq.client.HTTPClient wired up to
296        in-memory fakes of the Tahoe WebUI. Only a subset of the real
297        WebUI is available.
298    """
299
300    if root is None:
301        root = create_fake_tahoe_root()
302
303    client = HTTPClient(
304        agent=RequestTraversalAgent(root),
305        data_to_body_producer=_SynchronousProducer,
306    )
307    return client
Note: See TracBrowser for help on using the repository browser.